WordPress Cron API
Pseudo-cron system for scheduling deferred and recurring tasks in WordPress.
Since: 2.1.0
Source: wp-includes/cron.php, wp-cron.php
Components
| Component | Description |
|---|---|
| functions.md | Scheduling, unscheduling, and query functions |
| hooks.md | Filters for overriding cron behavior |
WP-Cron vs System Cron
| Aspect | WP-Cron | System Cron |
|---|---|---|
| Trigger | Page visits | OS scheduler |
| Reliability | Depends on traffic | Guaranteed timing |
| Precision | Approximate | Exact |
| Setup | Automatic | Requires server access |
| Load | Per-request overhead | Dedicated execution |
When WP-Cron Fails
- Low traffic sites: Events may not run until someone visits
- Cached pages: Full-page caching may prevent cron triggers
- Long-running processes: Previous cron still running (60-second lock)
Switching to System Cron
// wp-config.php
define( 'DISABLE_WP_CRON', true );
# System crontab (every minute)
* * * * * curl -s https://example.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1
# Or with WP-CLI
* * * * * cd /var/www/html && wp cron event run --due-now > /dev/null 2>&1
Spawn Mechanism
WP-Cron doesn’t run synchronously. Instead, it "spawns" a separate HTTP request:
Page Request
└── wp_cron() [on 'shutdown' action]
└── _wp_cron()
└── wp_get_ready_cron_jobs()
└── spawn_cron()
└── wp_remote_post( wp-cron.php )
Spawn Locking
Prevents multiple simultaneous spawns:
// Transient lock with 60-second timeout
$lock = get_transient( 'doing_cron' );
// Lock format: Unix timestamp with microseconds
$doing_wp_cron = sprintf( '%.22F', $gmt_time );
set_transient( 'doing_cron', $doing_wp_cron );
Lock conditions:
- Lock expires after 10 minutes (stale lock)
- New spawn blocked for 60 seconds after last spawn (
WP_CRON_LOCK_TIMEOUT)
Alternate Cron Mode
For servers that can’t make loopback requests:
// wp-config.php
define( 'ALTERNATE_WP_CRON', true );
Instead of spawning a background request, redirects the visitor to trigger cron:
Page Request
└── wp_cron() [on 'wp_loaded' action]
└── spawn_cron()
└── wp_redirect( ?doing_wp_cron=... )
└── require wp-cron.php
Cron Storage Structure
Events stored in wp_options table under cron option:
[
1707465600 => [ // Unix timestamp
'my_hourly_hook' => [
'a1b2c3d4...' => [ // MD5 of serialized args
'schedule' => 'hourly',
'args' => [],
'interval' => 3600,
],
],
],
1707469200 => [
'my_daily_hook' => [
'e5f6g7h8...' => [
'schedule' => 'daily',
'args' => ['post_id' => 123],
'interval' => 86400,
],
],
],
'version' => 2,
]
Key structure: $crons[timestamp][hook][args_hash]
Default Schedules
| Name | Interval | Constant |
|---|---|---|
hourly |
3,600 seconds | HOUR_IN_SECONDS |
twicedaily |
43,200 seconds | 12 * HOUR_IN_SECONDS |
daily |
86,400 seconds | DAY_IN_SECONDS |
weekly |
604,800 seconds | WEEK_IN_SECONDS |
Execution Flow (wp-cron.php)
wp-cron.php
├── Verify doing_wp_cron matches lock
├── _get_cron_array()
├── For each due event:
│ ├── wp_unschedule_event() [remove from array]
│ ├── wp_reschedule_event() [if recurring]
│ └── do_action_ref_array( $hook, $args )
└── Exit
Common Patterns
Schedule on Plugin Activation
register_activation_hook( __FILE__, 'my_plugin_activate' );
function my_plugin_activate() {
if ( ! wp_next_scheduled( 'my_plugin_cron_hook' ) ) {
wp_schedule_event( time(), 'hourly', 'my_plugin_cron_hook' );
}
}
add_action( 'my_plugin_cron_hook', 'my_plugin_cron_callback' );
function my_plugin_cron_callback() {
// Scheduled task logic
}
Clean Up on Deactivation
register_deactivation_hook( __FILE__, 'my_plugin_deactivate' );
function my_plugin_deactivate() {
wp_clear_scheduled_hook( 'my_plugin_cron_hook' );
}
One-Time Delayed Task
wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'my_delayed_task', [ $post_id ] );
add_action( 'my_delayed_task', function( $post_id ) {
// Runs once, ~1 hour from now
}, 10, 1 );
Debugging
WP-CLI Commands
# List scheduled events
wp cron event list
# Run due events now
wp cron event run --due-now
# Run specific hook
wp cron event run my_hook_name
# Test cron spawning
wp cron test
Check Cron Lock
$lock = get_transient( 'doing_cron' );
if ( $lock ) {
echo 'Cron locked until: ' . date( 'Y-m-d H:i:s', $lock + 60 );
}