WP-Cron isn’t a real cron job, and that trips up a lot of developers who assume it behaves like the Unix scheduler it’s named after. WordPress only checks for due cron events when a page on the site is loaded — no visitors, no page loads, no cron events firing, no matter how long has passed.

Why this matters on low-traffic sites

A site that gets a handful of visits a day might have a “daily” cron event actually fire every three or four days, whenever traffic happens to trigger the check. For anything time-sensitive — abandoned cart emails, subscription renewals — this delay is a real problem, not a theoretical one.

The fix: disable WP-Cron’s page-load trigger, use real cron

// In wp-config.php
define('DISABLE_WP_CRON', true);
# In the server's actual crontab, running every 5 minutes
*/5 * * * * curl -s https://example.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1

Now cron events run on a genuinely reliable schedule, independent of whether anyone’s visited the site recently.

When WP-Cron itself isn’t enough: Action Scheduler

WooCommerce ships with Action Scheduler, which is a proper job queue built on top of WordPress — it handles retries, queuing thousands of jobs without overwhelming the server at once, and logging failures in a way plain WP-Cron doesn’t:

as_schedule_single_action(time() + 300, 'myplugin_send_followup', ['order_id' => $order_id]);

add_action('myplugin_send_followup', function($order_id) {
    $order = wc_get_order($order_id);
    if ($order && $order->get_status() === 'completed') {
        // send the actual followup
    }
});

If Action Scheduler is already loaded (it is, on any site running WooCommerce), it’s usually the better choice over raw WP-Cron for anything beyond simple recurring events — particularly bulk operations, like emailing a large customer segment, where WP-Cron alone would try to do everything in one request and likely time out.