Why WordPress Cron Events Disappear (And How to Build Self-Healing Systems)
The Silent Killer of WordPress Automation
You’ve built a beautiful WordPress plugin. Your scheduled tasks work perfectly in development. You deploy to production, and everything runs smoothly… until it doesn’t.
One day, you discover your cron events simply stopped running. No errors. No warnings. Just silence.
This is one of the most frustrating aspects of WordPress development: WP-Cron is fundamentally unreliable, and scheduled events can vanish without a trace.
Why WordPress Cron Events Disappear
1. The Low-Traffic Problem
Unlike real cron systems that run on a schedule, WP-Cron is triggered by page visits. This creates a cascade of issues:
- On low-traffic sites, “every 5 minutes” might actually mean “every 3 hours”
- If an event is missed, WordPress doesn’t always reschedule it properly
- Events can fall into a state where they’re technically scheduled but never execute
2. Plugin Activation Dance
The most common culprit is the activation/deactivation lifecycle:
// On deactivation - events are cleared
register_deactivation_hook(__FILE__, 'cleanup_events');
// On activation - but what if this fails?
register_activation_hook(__FILE__, 'schedule_events');
If something goes wrong during activation (PHP error, memory limit, timeout), your events simply don’t get scheduled. The plugin appears to work, but the scheduler is dead.
3. The Nuclear Options
Many plugins use aggressive cleanup functions that can accidentally nuke other plugins’ events:
// Clears ALL instances of this hook, not just yours
wp_clear_scheduled_hook('process_queue');
// Removes the hook entirely from the system
wp_unschedule_hook('process_queue');
If another plugin uses similar hook names or runs overly aggressive cleanup, your events become collateral damage.
4. Database Optimization Gone Wrong
WordPress users love optimization plugins. Unfortunately, these tools often see cron entries as “orphaned data” and helpfully delete them:
- WP-Optimize cleaning “transients and cron jobs”
- Database cleanup tools removing “unused scheduled events”
- Manual database optimization removing rows from the
wp_optionstable
5. Caching Invalidation Issues
Object caching systems (Redis, Memcached) can serve stale cron data:
- WordPress checks if event is scheduled (reads from cache)
- Cache says “yes, it’s scheduled”
- Database actually says “no, it was cleared”
- WordPress never reschedules because the cached data is wrong
6. Code Updates and Hook Name Changes
You refactor your code and change some hook names:
// Old version
wp_schedule_event(time(), 'hourly', 'old_hook_name');
// New version
wp_schedule_event(time(), 'hourly', 'new_hook_name');
But the old events are still scheduled with the old hook name. They fire, nothing happens (no callback exists), and eventually the system gets confused.
7. Hosting Environment Sabotage
Some hosting providers “optimize” WordPress by:
- Setting
DISABLE_WP_CRONto true without telling you - Implementing aggressive PHP execution time limits
- Rate limiting requests to
wp-cron.php - Blocking the cron spawning mechanism entirely
Your code is perfect. The host just won’t let it run.
8. Race Conditions and Concurrency
On high-traffic sites, multiple simultaneous requests can create chaos:
- Two requests try to schedule the same event
- Duplicate detection logic triggers
- All instances get removed
- Event is now unscheduled
The Traditional “Solutions” (And Why They Fail)
Just Check on Activation
register_activation_hook(__FILE__, function() {
if (!wp_next_scheduled('my_hook')) {
wp_schedule_event(time(), 'hourly', 'my_hook');
}
});
Problem: Only runs when the plugin is activated. If events disappear later, they’re gone forever.
Check Before Running
add_action('init', function() {
if (!wp_next_scheduled('my_hook')) {
wp_schedule_event(time(), 'hourly', 'my_hook');
}
});
Problem: Runs on every page load. Performance nightmare. Still subject to race conditions.
Switch to Real Cron
// wp-config.php
define('DISABLE_WP_CRON', true);
Then add to system cron:
*/5 * * * * curl https://example.com/wp-cron.php >/dev/null 2>&1
Problem: Requires server access. Not feasible for public plugins. Still doesn’t solve events disappearing from the schedule.
The Self-Healing Solution
Instead of trying to prevent events from disappearing (impossible), build a system that automatically detects and repairs missing events.
The Core Concept
- Monitor – Regularly check if events are scheduled
- Detect – Identify missing events
- Heal – Automatically reschedule them
- Log – Track when healing happens so you can investigate root causes
The Critical Insight: Avoid Circular Dependencies
The common mistake:
// BAD: Hook healer to your own event
add_action('my_5_minute_event', 'check_if_events_scheduled');
If my_5_minute_event stops running, your healer never runs either. You’ve created a system that can’t fix itself.
The solution: Hook into WordPress core events that are more likely to remain active:
// GOOD: Hook to WordPress core events
add_action('wp_twicedaily_event', 'verify_and_heal_events');
WordPress core and countless plugins depend on events like wp_twicedaily_event, making them far more reliable than your custom events.
Implementation Pattern
class CronHealthCheck
{
// Define all your scheduled events
private static $events = [
'process_queue' => 'every_five_minutes',
'cleanup_temp' => 'hourly',
'send_digest' => 'daily',
];
public static function init()
{
// Primary healer - runs on WP core event
add_action('wp_twicedaily_event', [self::class, 'verify_and_heal']);
// Secondary healer - throttled admin check
add_action('admin_init', [self::class, 'maybe_admin_check']);
}
public static function verify_and_heal()
{
$healed = [];
foreach (self::$events as $hook => $recurrence) {
if (!wp_next_scheduled($hook)) {
wp_schedule_event(time(), $recurrence, $hook);
$healed[] = $hook;
}
}
if (!empty($healed)) {
error_log('Healed cron events: ' . implode(', ', $healed));
// Optional: Send notification, log to external service, etc.
}
}
public static function maybe_admin_check()
{
// Only check once per day in admin to avoid overhead
$last_check = get_transient('cron_health_admin_check');
if ($last_check === false) {
self::verify_and_heal();
set_transient('cron_health_admin_check', time(), DAY_IN_SECONDS);
}
}
}
CronHealthCheck::init();
Why This Works
Dual redundancy: The healer runs on two independent triggers:
- WordPress core event (reliable, runs even on low-traffic sites eventually)
- Admin dashboard loads (catches issues when admins are active)
No circular dependency: The healer doesn’t depend on the events it’s protecting.
Performance conscious: Admin checks are throttled to once per day.
Observable: Logs every healing action so you can track patterns.
Advanced: Diagnostic Logging
To understand why events are disappearing, add deeper logging:
// Track when events are unscheduled
add_action('wp_unschedule_event', function($timestamp, $hook, $args, $wp_error) {
if (in_array($hook, ['your_hook_1', 'your_hook_2'])) {
error_log(sprintf(
'Event unscheduled: %s | Backtrace: %s',
$hook,
wp_debug_backtrace_summary()
));
}
}, 10, 4);
// Track scheduling attempts
add_action('schedule_event', function($event) {
if (in_array($event->hook, ['your_hook_1', 'your_hook_2'])) {
error_log(sprintf(
'Event scheduled: %s at %s',
$event->hook,
date('Y-m-d H:i:s', $event->timestamp)
));
}
});
Run this for a week and check your logs. You’ll likely see patterns that reveal the root cause.
Best Practices Summary
- Always use self-healing checks – Don’t trust WP-Cron to stay reliable
- Hook to core events – Avoid circular dependencies
- Log everything – You can’t fix what you can’t see
- Throttle admin checks – Balance reliability with performance
- Monitor in production – Use error logs or external monitoring
- Consider real cron – For mission-critical tasks, system cron is more reliable
- Test low-traffic scenarios – Most issues appear on quiet sites
The Uncomfortable Truth
WordPress cron was designed for a simpler era. It’s a clever hack that works “well enough” for many use cases, but it’s fundamentally unreliable for anything mission-critical. The good news? By understanding the failure modes and building self-healing systems, you can create WordPress applications that survive WP-Cron’s quirks and keep running reliably for years.
Your scheduled tasks will still occasionally fail. But with proper health checks, they’ll automatically recover before anyone notices.