WordPress cron (WP-Cron) is not real cron. It is a visit-triggered system: scheduled tasks run when a visitor loads a page, not at a fixed time. On low-traffic sites, scheduled tasks may run hours late. On high-traffic sites, they may run multiple times simultaneously. WP Crontrol makes WP-Cron transparent and debuggable – and helps you set up real server cron to replace it.
How WP-Cron Actually Works
When WordPress generates any page, it checks whether any scheduled events are due. If yes, it spawns an HTTP request to wp-cron.php to run them. This has several implications:
- If no one visits your site, no scheduled tasks run – posts stay scheduled but do not publish
- The HTTP request to wp-cron.php is non-blocking but still consumes server resources on every page load
- High traffic means many simultaneous cron checks – tasks may fire multiple times if not written with concurrency in mind
- Server-level firewalls blocking loopback HTTP requests break WP-Cron entirely
Reading WP Crontrol
Go to Tools -> Cron Events. WP Crontrol shows every scheduled event with its next run time, schedule (once, hourly, daily, custom), and the hook name. Events marked in red are overdue – they should have run but have not. This is the clearest diagnostic signal that WP-Cron is not functioning.
The Cron Schedules tab shows all registered schedules. Check here if you are adding custom schedules and want to confirm they registered correctly.
Adding a Custom Cron Event
Two parts: register the schedule interval and hook the action:
// Register a custom interval (every 15 minutes)
add_filter('cron_schedules', function($schedules) {{
$schedules['every_15_minutes'] = array(
'interval' => 900,
'display' => 'Every 15 Minutes',
);
return $schedules;
}});
// Schedule the event on plugin/theme activation
function my_plugin_activate() {{
if (!wp_next_scheduled('my_custom_cron_hook')) {{
wp_schedule_event(time(), 'every_15_minutes', 'my_custom_cron_hook');
}}
}}
register_activation_hook(__FILE__, 'my_plugin_activate');
// The actual task
add_action('my_custom_cron_hook', function() {{
// Your task here
update_option('last_cron_run', current_time('mysql'));
}});
// Clean up on deactivation
function my_plugin_deactivate() {{
$timestamp = wp_next_scheduled('my_custom_cron_hook');
wp_unschedule_event($timestamp, 'my_custom_cron_hook');
}}
register_deactivation_hook(__FILE__, 'my_plugin_deactivate');
Need this built properly? Describe the project and get a free estimate.
Setting Up Real Server Cron
Replace WP-Cron’s visit-triggered system with a real server cron job that runs on a fixed schedule. This is the right solution for production sites.
Step 1: disable WP-Cron’s visit-triggered behaviour by adding to wp-config.php:
define('DISABLE_WP_CRON', true);
Step 2: add a real server cron job. Access your server’s crontab (cPanel -> Cron Jobs, or crontab -e via SSH):
# Run WordPress cron every minute
* * * * * cd /path/to/wordpress && php wp-cron.php > /dev/null 2>&1
# Or use WP-CLI if available (recommended):
* * * * * cd /path/to/wordpress && wp cron event run --due-now > /dev/null 2>&1
After setting up server cron, reload WP Crontrol and verify events are running on time. Events should show next run times updating regularly rather than stale overdue times.
Debugging Cron Jobs That Are Not Running
If WP Crontrol shows events as overdue and server cron is set up correctly, work through this checklist:
- Verify the cron job is actually running: add
> /tmp/cron.log 2>&1to the cron command and check the log file - Check PHP path:
phpin the cron command may point to a different PHP version than your site uses. Use the full path:/usr/local/bin/php8.2 - Verify the WordPress path in the cron command is correct
- Check for PHP errors in wp-cron.php execution by temporarily removing
> /dev/null 2>&1 - Confirm the hook has a callback registered – WP Crontrol shows the event but the hook may have no add_action registered for it