The Problem
Last Tuesday a client pinged me at 2 AM because their WooCommerce subscription renewal emails had not gone out for three days. The Action Scheduler queue was 14k jobs deep. Stripe was charging cards. Customers were getting receipts. But the membership emails, the post-renewal upsell flow, and the abandoned cart sequence had all stopped firing on the same date.
The site had been migrated to a managed WordPress host two weeks earlier. Scheduled tasks that worked fine on the old shared host died silently after the move. wp-cron.php was being blocked at the platform level and nobody had told the deploying team.
If your scheduled posts stopped publishing, transients are not clearing, Action Scheduler is backed up, or WP Mail SMTP shows nothing in the queue logs, the wp-cron pipeline is the suspect.
Why It Happens
WordPress ships with a pseudo-cron system called wp-cron.php that piggybacks on visitor traffic. Every front-end page load checks if anything is scheduled and runs it inline. That worked fine in 2008 when most sites had decent traffic and no full-page cache. In 2026 it falls over in three predictable ways:
- Managed hosts disable wp-cron and replace it with a real system cron — but only if you know about it. Kinsta, WP Engine, Pantheon, Pressable, and Cloudways all set
DISABLE_WP_CRONto true and call wp-cron.php from a real cron job every 15 minutes (60 on some plans). If you migrated from a host that did not do this, every short-interval task — including most Action Scheduler hooks — silently slows to 15-minute granularity. Jobs scheduled withwp_schedule_single_eventfor "now" wait 15 minutes minimum. - Page caching breaks the trigger. If 100% of traffic is served from Varnish, Cloudflare, or a static page cache, PHP never runs. wp-cron never fires. Sites with a CDN in front and aggressive cache rules can have wp-cron fail completely between deploys. I have seen sites go a week without a single cron firing because nobody was hitting an uncached URL.
- wp-cron.php blocks the request that triggers it. WordPress core uses a non-blocking HTTP request to call wp-cron.php on the same host. Some firewalls — looking at you, default Cloudflare WAF rules — block requests where the User-Agent is
WordPress/X.X; https://example.comand the destination isexample.com/wp-cron.php. The request 403s, and WordPress has no error reporting for cron failures.
The combination is what kills production. Managed host disables WP cron, expects you to use system cron, but the system cron is set to 15 minutes and your stack assumes 1-minute granularity.
The Fix
Step 1: Find out if WP cron is firing at all. Drop this in your mu-plugins directory as cron-debug.php:
<?php
// wp-content/mu-plugins/cron-debug.php
add_action('init', function () {
if (!current_user_can('manage_options')) return;
if (!isset($_GET['cron_debug'])) return;
$crons = _get_cron_array();
$next = wp_next_scheduled('action_scheduler_run_queue');
header('Content-Type: text/plain');
echo "DISABLE_WP_CRON: " . (defined('DISABLE_WP_CRON') && DISABLE_WP_CRON ? 'true' : 'false') . "\n";
echo "Next AS queue: " . ($next ? date('c', $next) : 'NOT SCHEDULED') . "\n";
echo "Total scheduled: " . count($crons) . "\n\n";
foreach ($crons as $ts => $hooks) {
printf(" %s — %s\n", date('c', $ts), implode(', ', array_keys($hooks)));
}
exit;
});
Visit /?cron_debug=1 as an admin. If DISABLE_WP_CRON: true and the next AS queue timestamp is in the past, the system cron is not actually calling wp-cron.php.
Step 2: Confirm the system cron is wired. SSH in and run:
crontab -l | grep wp-cron
You want to see something like:
* * * * * curl -s https://example.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1
If the schedule is */15 or it is missing entirely, that is the bug. On Kinsta and WP Engine you cannot edit crontab directly — you have to request a custom cron schedule or use their dashboard. On a VPS or dedicated server, edit the crontab yourself:
crontab -e
# add:
* * * * * cd /var/www/html && /usr/bin/php wp-cron.php > /dev/null 2>&1
Prefer the WP-CLI route over curl when the host firewall might be blocking self-requests:
* * * * * cd /var/www/html && wp cron event run --due-now --quiet
Step 3: Bypass the firewall problem. If you must use curl and Cloudflare is blocking the self-request, add a page rule that bypasses WAF for /wp-cron.php, or send a custom header your origin trusts:
* * * * * curl -s -H "X-Cron-Secret: $(cat /etc/wp-cron-secret)" \
https://example.com/wp-cron.php > /dev/null 2>&1
And in mu-plugins/cron-allow.php:
add_filter('cron_request', function ($args) {
$secret_file = '/etc/wp-cron-secret';
if (is_readable($secret_file)) {
$args['args']['headers']['X-Cron-Secret'] = trim(file_get_contents($secret_file));
}
return $args;
});
Step 4: For Action Scheduler-heavy sites, give AS its own runner. WooCommerce and most modern plugins use Action Scheduler now, which queues jobs through the WP cron loop by default. If you have thousands of pending actions, run AS directly via WP-CLI every minute and bypass wp-cron for that workload entirely:
* * * * * cd /var/www/html && wp action-scheduler run --batches=10 --batch-size=100 --quiet
This keeps your subscription renewals, email queues, and webhook retries moving even if wp-cron is still misbehaving. The official Action Scheduler WP-CLI docs cover the flags and concurrency options.
Step 5: Monitor it so this never bites you again. Use a heartbeat. Schedule a single recurring event that pings a healthcheck URL, and alert if the ping stops:
add_action('init', function () {
if (!wp_next_scheduled('site_cron_heartbeat')) {
wp_schedule_event(time(), 'hourly', 'site_cron_heartbeat');
}
});
add_action('site_cron_heartbeat', function () {
wp_remote_get('https://hc-ping.com/your-uuid-here', ['timeout' => 5]);
});
If healthchecks.io stops getting pings, cron is broken and you find out in an hour, not three days later.
The Lesson
wp-cron is not a cron daemon, it is a poll-on-traffic. The moment you put a real cache in front of your site or move to a managed host, you need a system cron calling wp-cron.php or running WP-CLI directly, plus a monitor that tells you when it stops. Verify the schedule on the host, confirm the firewall is not blocking self-requests, and give Action Scheduler its own runner if your business depends on it.
If your WooCommerce site has stuck scheduled actions or you suspect cron is silently failing on a managed host, that is the kind of forensic work I do for clients — see my services. For a related Action Scheduler pattern I covered, see WooCommerce Action Scheduler stuck in progress.