wp-cron
WordPress’s built-in cron (wp-cron) runs scheduled tasks — sending emails, fetching feeds, cleaning expired data. It’s NOT a real cron; it fires on page loads. For reliable scheduling, disable it and run via real cron + WP-CLI.
wp_schedule_event, real cron, debugging
EXAMPLE
<?php
// 1) Schedule a recurring event
add_action( 'init', function () {
if ( ! wp_next_scheduled( 'my_daily_cleanup' ) ) {
wp_schedule_event( time(), 'daily', 'my_daily_cleanup' );
}
} );
// Hook the actual work
add_action( 'my_daily_cleanup', function () {
global $wpdb;
$deleted = $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_old_%' AND option_value < UNIX_TIMESTAMP()" );
error_log( "Cleaned {$deleted} expired transients" );
} );
// 2) Built-in schedules
// • hourly — every hour
// • twicedaily — every 12 hours
// • daily — every 24 hours
// • weekly — every 7 days (since WP 5.4)
// 3) Custom interval
add_filter( 'cron_schedules', function ( $schedules ) {
$schedules['every_15_min'] = array(
'interval' => 15 * MINUTE_IN_SECONDS,
'display' => 'Every 15 minutes',
);
$schedules['every_5_min'] = array(
'interval' => 5 * MINUTE_IN_SECONDS,
'display' => 'Every 5 minutes',
);
return $schedules;
} );
// Now you can use 'every_15_min' in wp_schedule_event.
// 4) One-off scheduled event
wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'send_followup_email', array( $user_id ) );
add_action( 'send_followup_email', function ( $user_id ) {
$user = get_userdata( $user_id );
wp_mail( $user->user_email, 'Welcome back', '…' );
} );
// 5) Pass arguments
wp_schedule_event( time(), 'daily', 'sync_with_api', array( 'us-east' ) );
add_action( 'sync_with_api', function ( $region ) {
// perform sync for region
} );
// 6) Unschedule
$timestamp = wp_next_scheduled( 'my_daily_cleanup' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'my_daily_cleanup' );
}
// Unschedule with arguments (must match exactly)
wp_clear_scheduled_hook( 'sync_with_api', array( 'us-east' ) );
// Cleanup on plugin deactivation
register_deactivation_hook( __FILE__, function () {
wp_clear_scheduled_hook( 'my_daily_cleanup' );
wp_clear_scheduled_hook( 'sync_with_api' );
} );
// 7) The wp-cron PROBLEM
// wp-cron fires on PAGE LOADS. If your site has no traffic, scheduled tasks NEVER RUN.
// Even with traffic, timing is imprecise (can fire late or repeatedly).
// On high-traffic sites it can run per request, slowing down responses.
// 8) Disable wp-cron + use real cron — recommended for production
// wp-config.php
define( 'DISABLE_WP_CRON', true );
// Then add a real cron job (Linux server crontab):
// crontab -e
// */5 * * * * curl -s 'https://example.com/wp-cron.php?doing_wp_cron' > /dev/null 2>&1
// Or with WP-CLI (preferred — no HTTP request needed):
// */5 * * * * cd /var/www/html && wp cron event run --due-now > /dev/null 2>&1
// 9) WP-CLI commands
wp cron event list # see all scheduled events
wp cron event run my_daily_cleanup # fire NOW
wp cron event run --due-now # fire all due events
wp cron event delete my_daily_cleanup
wp cron event schedule my_daily_cleanup now daily
wp cron schedule list # available schedules
wp cron test # check if wp-cron is reachable
// 10) Debug — what's scheduled?
// Install WP Crontrol plugin → Tools → Cron Events
// Or programmatically:
$crons = _get_cron_array();
foreach ( $crons as $timestamp => $cron ) {
foreach ( $cron as $hook => $events ) {
echo $hook . ' next at ' . wp_date( 'Y-m-d H:i:s', $timestamp ) . "\n";
}
}
// 11) Error handling + logging
add_action( 'my_task', function () {
try {
$result = expensive_operation();
update_option( 'my_task_last_run', time() );
} catch ( \Throwable $e ) {
error_log( 'my_task failed: ' . $e->getMessage() );
if ( function_exists( 'wp_mail' ) ) {
wp_mail( get_option( 'admin_email' ), 'Cron failure', $e->getMessage() );
}
}
} );
// 12) Timeout + memory
// wp-cron requests can be killed by PHP max_execution_time (default 30s).
// For long jobs:
// • Use Action Scheduler (used by WooCommerce; better at scale)
// • Chunk: schedule next single event from inside the handler
// • Increase PHP timeout in cron config
// 13) Action Scheduler — robust alternative
// composer require woocommerce/action-scheduler
as_schedule_recurring_action(
time(), // first run
HOUR_IN_SECONDS, // interval
'my_recurring_job',
array( /* args */ ),
'my_group',
);
// • Stored in database; survives missed runs
// • Batch processing built in
// • Dashboard in WP admin
// 14) Common bugs
// • Forgot to check wp_next_scheduled → events scheduled MULTIPLE TIMES on every load
// • Hook on 'init' so cron registration on every request → fine + idempotent if wp_next_scheduled used
// • Unschedule with mismatched args → cleared the wrong event; pass exact args
// • Calling expensive code in a request-handling cron event → slow page loads; use real cron
// • DISABLE_WP_CRON without setting up real cron → events NEVER fire
// • Plugin deactivated but events linger → register_deactivation_hook
// • Args not serialisable → fatal error scheduling
// • Job runs but next iteration overlaps → use lock (transient or option)
// • Forgetting timezone — schedule with UTC timestamp
// • Errors in cron handler silently swallowed — always log
Why it matters
WP-cron runs on page loads, not on a real schedule. For anything important, define(\"DISABLE_WP_CRON\", true) and run a real cron entry pointing at wp cron event run --due-now. Always guard wp_schedule_event with wp_next_scheduled to avoid duplicates, register a deactivation cleanup, and reach for Action Scheduler when you need reliable, batch-processed jobs at scale.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
if (!wp_next_scheduled('my_cron')) {
wp_schedule_event(time(), 'hourly', 'my_cron');
}
add_action('my_cron', 'do_work');
Try it Yourself »
Discussion
Loading…