Free ServiceCronRadar is completely free for the first half of 2026!

How to Monitor WordPress Cron Jobs (WP-Cron)

Cronradar··7 min

WordPress powers over 40% of the web, yet its built-in scheduling system has a fundamental flaw: it only runs when someone visits your site. This "virtual cron" approach means your scheduled backups, WooCommerce subscription renewals, and time-sensitive posts can silently fail—and you won't know until it's too late.

This guide explains why WP-Cron fails, how to set up reliable server-side cron, and how to implement external monitoring that actually alerts you when things break.

Why WP-Cron Fails (The Traffic Problem)

Unlike traditional server cron jobs that run on a fixed schedule, WordPress implements a pseudo-cron system triggered by page visits. Here's the sequence:

  1. A visitor loads any page on your site
  2. WordPress checks if any scheduled tasks are overdue
  3. If tasks are due, WordPress spawns a background HTTP request to wp-cron.php
  4. That separate request executes the pending tasks

The problem is obvious: no visitors = no cron execution.

WordPress's own documentation acknowledges this: if you schedule a task for 2:00 PM but no one visits until 5:00 PM, your task runs three hours late—or not at all.

This design causes real-world failures:

  • Missed backups — Your UpdraftPlus backup scheduled for 3 AM never runs because no one browses your site at night
  • Failed subscription payments — WooCommerce Subscriptions can't process renewals, leaving revenue on the table
  • Unpublished scheduled posts — Your carefully timed product launch sits in "Missed Schedule" limbo
  • Stale security scans — Wordfence scheduled scans don't run, leaving vulnerabilities undetected

The situation gets worse with page caching. If your caching plugin serves static HTML, requests never reach PHP—and WP-Cron never triggers.

Step 1: Disable the Default WP-Cron

The first step toward reliable WordPress scheduling is disabling the traffic-dependent default behavior.

Add this line to your wp-config.php file, before the line that says "That's all, stop editing!":

define( 'DISABLE_WP_CRON', true );

This prevents WordPress from spawning cron checks on every page load. You'll replace this with a proper server-side trigger.

Alternative Constants

Two other constants handle edge cases:

// Use if your server blocks loopback connections (403 errors on wp-cron.php)
define( 'ALTERNATE_WP_CRON', true );

// Increase lock timeout for sites with long-running tasks (default: 60 seconds)
define( 'WP_CRON_LOCK_TIMEOUT', 120 );

Only use ALTERNATE_WP_CRON if you're seeing failed cron spawns in your logs—it redirects the visitor's browser instead of making a background HTTP request, which can cause slight delays.

Step 2: Set Up Server-Side Cron

With default WP-Cron disabled, you need a real cron job to trigger WordPress tasks. You have three options depending on your hosting environment.

Option A: Using wget

*/15 * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

Option B: Using curl

*/15 * * * * curl --max-time 30 -s https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

The --max-time 30 flag prevents hung requests from blocking your crontab.

If you have shell access and WP-CLI installed, this is the most reliable method because it bypasses HTTP entirely:

*/15 * * * * cd /var/www/html && /usr/local/bin/wp cron event run --due-now --quiet

Replace /var/www/html with your WordPress installation path.

Choosing an Interval

Most sites work well with 15-minute intervals. Consider shorter intervals (5 minutes) if you have:

  • WooCommerce Subscriptions with frequent renewals
  • Time-sensitive scheduled posts
  • High-volume email queues

Managed WordPress hosts like Kinsta and WP Engine automatically set up server cron, but their intervals vary (15 minutes to 1 hour). Check your host's documentation.

Step 3: Verify Cron Is Running

Setting up server cron doesn't guarantee it's working. Test with WP-CLI:

# List all scheduled events
wp cron event list

# Show overdue events (should be empty or minimal)
wp cron event list --fields=hook,next_run_relative | grep -i ago

# Manually trigger due events
wp cron event run --due-now

You can also check programmatically by adding this to your theme's functions.php or a custom plugin:

function get_overdue_cron_count() {
    $crons = _get_cron_array();
    $overdue = 0;
    
    foreach ( $crons as $timestamp => $hooks ) {
        if ( $timestamp < time() ) {
            $overdue += count( $hooks );
        }
    }
    
    return $overdue;
}

// Usage: Check in admin dashboard
add_action( 'admin_notices', function() {
    $overdue = get_overdue_cron_count();
    if ( $overdue > 5 ) {
        echo '<div class="notice notice-warning"><p>';
        echo sprintf( 'Warning: %d cron events are overdue. Check your server cron configuration.', $overdue );
        echo '</p></div>';
    }
});

The Problem with Plugin-Based Monitoring

At this point, you might consider installing WP Crontrol or Advanced Cron Manager. These plugins let you view, edit, and manually run cron events from your dashboard.

But they have a fundamental limitation: they only work when WordPress works.

If your site crashes, your database goes down, or PHP throws a fatal error, these plugins can't alert you. They're running inside the same system that failed.

Common monitoring gaps:

Approach What it catches What it misses
WP Crontrol Overdue events (when you check) Silent failures, site-down scenarios
Server cron logs Whether the HTTP request succeeded Whether the task actually completed
Uptime monitoring Site availability Individual cron job failures

For mission-critical tasks—backups, payment processing, scheduled content—you need monitoring that works independently of WordPress.

Step 4: Add External Monitoring with Webhooks

External monitoring works by having your cron jobs "ping" an outside service when they complete. If the ping doesn't arrive on schedule, you get an alert.

Creating a Monitoring Wrapper

Wrap your important cron callbacks to send pings on completion:

// Add to wp-config.php
define( 'CRONRADAR_PING_URL', 'https://cronradar.com/ping/your-unique-id' );

// In your plugin or functions.php
function monitored_backup_task() {
    $start_time = microtime( true );
    
    try {
        // Your actual backup logic here
        run_backup_process();
        
        // Ping on success
        $duration = round( microtime( true ) - $start_time, 2 );
        wp_remote_get( CRONRADAR_PING_URL . '?duration=' . $duration, array(
            'timeout'  => 10,
            'blocking' => false,
        ));
        
    } catch ( Exception $e ) {
        // Ping failure endpoint
        wp_remote_get( CRONRADAR_PING_URL . '/fail?error=' . urlencode( $e->getMessage() ), array(
            'timeout'  => 10,
            'blocking' => false,
        ));
        
        error_log( 'Backup failed: ' . $e->getMessage() );
        throw $e;
    }
}

The blocking => false parameter ensures the ping doesn't slow down your cron execution.

Heartbeat Monitoring Pattern

For general WP-Cron health, set up a heartbeat that pings every few minutes:

// Register a 5-minute interval
add_filter( 'cron_schedules', function( $schedules ) {
    $schedules['five_minutes'] = array(
        'interval' => 300,
        'display'  => 'Every 5 Minutes',
    );
    return $schedules;
});

// Schedule the heartbeat (run once on activation)
register_activation_hook( __FILE__, function() {
    if ( ! wp_next_scheduled( 'cronradar_heartbeat' ) ) {
        wp_schedule_event( time(), 'five_minutes', 'cronradar_heartbeat' );
    }
});

// Send the heartbeat ping
add_action( 'cronradar_heartbeat', function() {
    if ( defined( 'CRONRADAR_HEARTBEAT_URL' ) ) {
        wp_remote_get( CRONRADAR_HEARTBEAT_URL, array(
            'timeout'  => 10,
            'blocking' => false,
        ));
    }
});

// Clean up on deactivation
register_deactivation_hook( __FILE__, function() {
    wp_clear_scheduled_hook( 'cronradar_heartbeat' );
});

If CronRadar doesn't receive the heartbeat within your expected window (say, 10 minutes), it alerts you that WordPress cron has stopped working entirely.

Step 5: Build a Health Check Endpoint

For more detailed monitoring, create a REST API endpoint that external services can query:

add_action( 'rest_api_init', function() {
    register_rest_route( 'cron-monitor/v1', '/health', array(
        'methods'             => 'GET',
        'callback'            => 'cron_health_check',
        'permission_callback' => '__return_true',
    ));
});

function cron_health_check( WP_REST_Request $request ) {
    $crons = _get_cron_array();
    $now = time();
    $overdue_events = array();
    $upcoming_events = array();
    
    foreach ( $crons as $timestamp => $hooks ) {
        foreach ( $hooks as $hook => $args ) {
            $event = array(
                'hook'      => $hook,
                'scheduled' => date( 'c', $timestamp ),
                'overdue'   => $timestamp < $now,
            );
            
            if ( $timestamp < $now ) {
                $overdue_events[] = $event;
            } else {
                $upcoming_events[] = $event;
            }
        }
    }
    
    $status = 'healthy';
    if ( count( $overdue_events ) > 10 ) {
        $status = 'critical';
    } elseif ( count( $overdue_events ) > 3 ) {
        $status = 'warning';
    }
    
    return new WP_REST_Response( array(
        'status'          => $status,
        'overdue_count'   => count( $overdue_events ),
        'overdue_events'  => array_slice( $overdue_events, 0, 10 ),
        'upcoming_count'  => count( $upcoming_events ),
        'wp_cron_disabled'=> defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON,
        'checked_at'      => date( 'c' ),
    ), 200 );
}

Access this endpoint at https://yoursite.com/wp-json/cron-monitor/v1/health.

Adding Authentication

For production sites, secure the endpoint with a token:

function cron_health_check( WP_REST_Request $request ) {
    $token = $request->get_header( 'X-Monitor-Token' );
    $expected_token = defined( 'CRON_MONITOR_TOKEN' ) ? CRON_MONITOR_TOKEN : '';
    
    if ( empty( $expected_token ) || ! hash_equals( $expected_token, $token ) ) {
        return new WP_Error( 'unauthorized', 'Invalid token', array( 'status' => 401 ) );
    }
    
    // ... rest of the function
}

Add the token to your wp-config.php:

define( 'CRON_MONITOR_TOKEN', 'your-secret-token-here' );

Critical Cron Hooks to Monitor

Not all cron jobs are equal. Prioritize monitoring based on business impact:

High Priority (Revenue & Security)

Hook Source Failure Impact
woocommerce_scheduled_subscription_payment WooCommerce Subscriptions Missed recurring revenue
updraft_backup UpdraftPlus No backup recovery point
wordfence_start_scheduled_scan Wordfence Security vulnerabilities missed
publish_future_post WordPress Core Scheduled content not published
woocommerce_scheduled_sales WooCommerce Sale prices don't activate

Medium Priority (Operations)

Hook Source Failure Impact
wp_version_check WordPress Core Miss security updates
woocommerce_cancel_unpaid_orders WooCommerce Inventory held incorrectly
fluentcrm_process_email_queue FluentCRM Marketing emails delayed
wp_scheduled_delete WordPress Core Trash not emptied, database bloat

WooCommerce Action Scheduler

WooCommerce 3.0+ uses Action Scheduler instead of standard WP-Cron for better reliability. Check its status at WooCommerce → Status → Scheduled Actions.

Warning signs:

  • Pending actions older than 24 hours
  • Large number of failed actions
  • "Scheduled Action Timeout" notices

Putting It All Together

Here's a complete monitoring setup for a WooCommerce site:

  1. Disable default WP-Cron in wp-config.php
  2. Set up server cron at 5-minute intervals via WP-CLI
  3. Add heartbeat monitoring that pings CronRadar every 5 minutes
  4. Wrap critical tasks (backups, subscription payments) with success/failure pings
  5. Create a health endpoint for detailed status checks
  6. Configure alerts in CronRadar for:
    • Missed heartbeats (cron stopped entirely)
    • Failed task pings (specific job errors)
    • Health endpoint returning "critical" status

This layered approach catches failures at every level—from WP-Cron stopping entirely to individual tasks throwing errors.

Common Troubleshooting

Cron runs but tasks still fail

Check for PHP fatal errors in your error log. A crashed callback doesn't notify WP-Cron that it failed.

"Missed Schedule" on scheduled posts

Usually means WP-Cron wasn't running when the post was due. Verify server cron is configured and check for caching conflicts.

Backups always overdue

Backup plugins like UpdraftPlus can run for 10+ minutes. If another cron tries to spawn during the lock timeout, it gets skipped. Increase WP_CRON_LOCK_TIMEOUT or schedule backups during low-traffic periods.

WooCommerce subscriptions not processing

Check Action Scheduler status. If actions are stuck in "pending," your server cron might not be triggering action_scheduler_run_queue.

Summary

WordPress's built-in cron system was designed for simplicity, not reliability. For sites where scheduled tasks matter—backups, e-commerce, time-sensitive content—you need:

  1. Server-side cron to trigger tasks reliably (not traffic-dependent)
  2. External monitoring to alert you when tasks fail
  3. Proper instrumentation of critical jobs with success/failure pings

The combination of DISABLE_WP_CRON, a proper server crontab, and external ping monitoring gives you the reliability that WP-Cron alone can't provide.


Monitor your WordPress cron jobs with CronRadar. Get instant alerts when scheduled tasks fail—even if WordPress itself goes down. Start free monitoring →

Start monitoring your cron jobs

Get started in minutes. No credit card required.