WP-Cron Failures on WooCommerce: Missing Emails, Failed Renewals, and the System Cron Fix

· 13 min read

The Symptoms: Customers Complaining About Missing Order Emails

A UK-based subscription box company running WooCommerce contacted me after their customers started complaining. Order confirmation emails were arriving hours late — sometimes not at all. Subscription renewals were failing silently. And when I checked the Action Scheduler queue, there were over 4,200 pending actions, some dating back three weeks.

The store was processing around 150 orders per day, running WooCommerce Subscriptions, and hosted on a managed VPS with CloudPanel. On paper, the server was fine — 4 vCPUs, 8GB RAM, PHP 8.2, MariaDB 10.11. No resource bottleneck.

The problem was WP-Cron. And the worst part of a WP-Cron failure is that nothing in WordPress tells anyone. It doesn't log a warning or send an alert. It just silently queues everything up and waits.

What WP-Cron Actually Does (And Why It Fails)

WordPress doesn't have a real task scheduler. Instead, it uses a pseudo-cron system called WP-Cron that only fires when someone visits the site. Every page load, WordPress checks if any scheduled tasks are overdue and runs them if so. No page load, no cron.

This works fine for a small blog. It falls apart for WooCommerce stores, because WooCommerce relies on scheduled events for the things that actually make money: transactional emails (order confirmations, shipping notifications, renewal reminders), subscription renewals via Action Scheduler, scheduled sales and price changes, webhook deliveries, and session/transient cleanup.

Three things break the page-load trigger in practice:

  1. Full-page caching prevents triggers. If your caching layer serves a static page, PHP never executes, and WP-Cron never fires. This store was running LiteSpeed Cache with Guest Optimization enabled — the very feature I wrote about in a previous post on LiteSpeed FOUC. Most page loads never hit PHP at all.

  2. Traffic patterns create gaps. Between 11pm and 7am, this site had almost no visitors. Any scheduled task due during those hours — like a midnight subscription renewal batch — simply didn't run until the next morning.

  3. WP-Cron is single-threaded per request. When it does fire, it processes one batch of tasks per page load. If you have 4,200 pending actions, that's going to take a very long time to clear at one batch per visitor.

Diagnosing the Problem

The first thing I did was SSH into the server and check the Action Scheduler status via WP-CLI:

wp action-scheduler list --status=pending --per-page=20 --orderby=scheduled --order=asc

The oldest pending action was 22 days old. That's 22 days of subscription renewals, email dispatches, and webhook deliveries sitting in a queue.

I then checked for failed actions:

wp action-scheduler list --status=failed --per-page=20

Over 300 failed actions, mostly woocommerce_scheduled_subscription_payment and wc_admin_process_orders_milestone. The failure reason on most was a timeout — the action started but WP-Cron's execution window closed before it could finish.

Next, I checked whether WP-Cron was being disabled or misconfigured:

wp config get DISABLE_WP_CRON 2>/dev/null || echo "Not set"

Output: Not set. So WP-Cron was theoretically active — it just wasn't being triggered often enough thanks to the caching layer.

I verified this by checking when WP-Cron was next due to run:

wp option get cron --format=json | python3 -c "
import json, sys, datetime
data = json.load(sys.stdin)
timestamps = [int(k) for k in data.keys() if k.isdigit()]
if timestamps:
    latest = max(timestamps)
    dt = datetime.datetime.fromtimestamp(latest)
    print(f'Next scheduled cron event: {dt}')
"

The next scheduled event was hours overdue. WP-Cron was effectively dead.

The Other Way This Fails: DISABLE_WP_CRON With No Replacement

The day before, I'd fixed the mirror image of this problem on another client's store. Same symptoms — a Monday morning message: "Customers are saying they're not getting order confirmation emails. Some since Friday." Payments were going through and stock was decrementing, but no emails, and weekend subscription renewals were sitting in limbo.

On that site, the same wp config get DISABLE_WP_CRON check returned true. Someone — possibly a previous developer, possibly the hosting provider — had disabled WP-Cron but never set up a replacement. This is the single most common cause of silent cron failures I see on client sites.

wp cron event list is the quickest way to see the damage:

wp cron event list --fields=hook,next_run_relative,recurrence --format=json | \
  python3 -c "
import json, sys, time
events = json.load(sys.stdin)
now = time.time()
overdue = [e for e in events if e['time'] < now]
print(f'{len(overdue)} overdue events')
"

42 overdue events, the oldest 72 hours behind. Hooks like woocommerce_scheduled_sales, action_scheduler_run_queue, and wc_admin_daily were all stuck, along with days of pending woocommerce_scheduled_subscription_payment actions. Every hour that went unfixed was lost revenue.

The site health loopback test was actually passing — but it didn't matter, because with DISABLE_WP_CRON set to true, WordPress wouldn't fire events even with traffic hitting the site. And crontab -l -u www-data came back empty: the previous developer had disabled WP-Cron (probably to fix a performance issue) and either forgot to add a system cron or assumed the hosting provider would handle it.

Two sites, two different root causes, one conclusion: the page-load trigger cannot be trusted, and the fix is the same for both.

The Fix: A Proper System Cron

The fix has three parts: disable WP-Cron's page-load trigger, set up a real system cron job, and clear the backlog.

Step 1: Disable WP-Cron in wp-config.php

wp config set DISABLE_WP_CRON true --raw

This adds define('DISABLE_WP_CRON', true); to wp-config.php. It stops WordPress from trying to run cron on every page load — which also removes a small performance overhead from each request. On a WooCommerce store doing hundreds of requests per minute, that's hundreds of unnecessary cron checks eliminated. And to be clear: you keep this set to true permanently. The point of the earlier incident isn't that disabling WP-Cron is wrong — it's that disabling it without a replacement is.

Step 2: Create a System Cron Job

On a Linux server, you create a crontab entry at a fixed interval. There are two approaches — I prefer WP-CLI over curl because it doesn't depend on the web server or loopback requests.

crontab -e

Add this line:

*/5 * * * * cd /home/cloudpanel/htdocs/example.com && /usr/local/bin/wp cron event run --due-now --quiet >> /var/log/wp-cron.log 2>&1

This runs every 5 minutes, regardless of site traffic or caching. The --due-now flag processes only events that are currently due, and --quiet suppresses output unless there's an error.

For the Action Scheduler specifically, I added a second cron job to process its queue directly:

*/5 * * * * cd /home/cloudpanel/htdocs/example.com && /usr/local/bin/wp action-scheduler run --batch-size=50 --quiet >> /var/log/wp-action-scheduler.log 2>&1

This is important because Action Scheduler has its own queue runner that's more efficient than relying on WP-Cron events to trigger it.

A note on timing: Every 5 minutes is right for most WooCommerce stores. If you're running a high-volume store (500+ orders/day) or time-sensitive subscriptions, you might want every 2 minutes. For low-traffic brochure sites, every 15 minutes is fine.

Step 3: Clear the Backlog

With 4,200 pending actions, I didn't want to wait for the cron job to chew through them at 50 per batch. I ran the queue manually:

wp action-scheduler run --batch-size=200 --batches=0 --force

The --batches=0 flag tells it to keep going until the queue is empty, and --force overrides the concurrent batch limiter. On this server, it took about 12 minutes to process all 4,200 actions.

One caution from the other incident: if the backlog contains pending subscription payments, don't just fire everything at maximum speed. On that site I ran the queue with --batch-size=10, monitoring server load, checking emails were going out correctly, and verifying each subscription payment in WooCommerce to make sure the payment gateway processed it properly. A three-day backlog flushed carelessly can flood customers with stale emails and hammer your mail server.

Back on the subscription box store, I also cleaned up the failed actions that were no longer recoverable:

wp action-scheduler clean --status=failed --before="2026-03-15 00:00:00"

For the actions that had failed within the last few days — particularly subscription renewals — I identified the affected subscriptions and triggered manual renewals:

wp wc shop_subscription list --status=on-hold --format=ids | xargs -I {} wp wc shop_subscription update {} --status=active

Verifying It Works

After setting up the system cron, I monitored the Action Scheduler queue over the next 24 hours:

wp action-scheduler list --status=pending --format=count

Within the first hour, the pending count dropped from the residual backlog to under 20 — all of which were legitimately scheduled for the future. The queue was healthy.

A system cron can also fail silently — the crontab can get wiped during a server migration, or the WP-CLI binary can break after a PHP upgrade. So I chained a Healthchecks.io ping onto the cron run:

*/5 * * * * cd /home/cloudpanel/htdocs/example.com && /usr/local/bin/wp cron event run --due-now --quiet && curl -fsS -m 10 --retry 3 https://hc-ping.com/your-uuid-here > /dev/null 2>&1

If the ping stops arriving, Healthchecks.io alerts me via Slack. This catches scenarios like the server running out of disk space, PHP-FPM crashing, or the WP-CLI binary being removed during a server update. It's the part most guides skip, and it's the most important part for a maintenance provider — you need to know before the client does.

Common Variations of This Problem

After fixing dozens of these across client sites, I've seen a few recurring patterns beyond the two incidents above:

Hosting provider disabled it silently. Some shared hosts disable WP-Cron to reduce server load. They don't always tell you. Check wp-config.php on any new site you take over.

Security plugin blocking loopback. Wordfence, Sucuri, and similar plugins can block the internal HTTP request that WP-Cron uses to spawn. If DISABLE_WP_CRON is false but events still aren't running, check your security plugin's firewall rules.

ALTERNATE_WP_CRON masking the real problem. Some developers add define('ALTERNATE_WP_CRON', true) as a band-aid. This uses a redirect-based approach instead of the async spawn. It sort of works, but adds latency to every page load and can confuse caching plugins. A system cron is always the better fix.

BasicAuth or IP restrictions blocking wp-cron.php. Staging sites with HTTP authentication will block the internal cron request. Either use WP-CLI cron or whitelist wp-cron.php in your server config:

location = /wp-cron.php {
    satisfy any;
    allow all;
    fastcgi_pass unix:/run/php/php-fpm.sock;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}

Why This Matters More Than You Think

On a basic WordPress blog, a missed cron event means a scheduled post publishes an hour late. Annoying, not catastrophic.

On a WooCommerce store with subscriptions, missed cron events mean:

  • Failed subscription renewals — customers don't get charged, you lose revenue, and WooCommerce puts subscriptions on hold
  • Delayed order emails — customers think their order didn't go through and either contact support or place a duplicate order
  • Action Scheduler backlog — webhook deliveries to fulfilment partners pile up, causing shipping delays
  • Stale analytics — WooCommerce Admin background processing falls behind, making your dashboard unreliable
  • Failed scheduled sales — sale prices that should start or end at specific times don't update on schedule

The subscription box store was losing an estimated 8-12 subscription renewals per week to timing failures. At their average subscription value, that was over GBP 400/month in quietly lost revenue. This is exactly the kind of revenue-critical failure my WooCommerce maintenance plans exist to catch.

The Formula for Your Server

Here's my standard setup for WooCommerce stores on VPS or dedicated servers:

Server type Cron interval Action Scheduler batch size
Low-traffic (< 50 orders/day) Every 15 minutes 25
Medium-traffic (50-200 orders/day) Every 5 minutes 50
High-traffic (200+ orders/day) Every 2 minutes 100

Always use WP-CLI over curl/wget for the cron trigger. The WP-CLI approach runs in the PHP CLI context, which means no dependency on the web server or loopback requests, no interference from security plugins that block wp-cron.php, longer execution time limits, and no overhead from loading the full front-end stack.

If you're on managed WordPress hosting, this is usually handled for you — WP Engine and Kinsta run system-level cron jobs automatically; Cloudways requires manual configuration through their dashboard. On shared cPanel hosting where WP-CLI isn't available, add a cron job through cPanel's interface using the curl method instead:

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

Less reliable than WP-CLI, but significantly better than relying on visitor traffic.

What I Set Up on Every Maintenance Client

Every site I onboard gets the same cron configuration:

  1. DISABLE_WP_CRON set to true in wp-config.php
  2. System cron running wp cron event run --due-now every 5 minutes
  3. Healthchecks.io monitoring on the cron job
  4. WP Crontrol plugin installed for visibility into scheduled events

It takes 10 minutes to set up and prevents both scenarios in this post — weeks of degraded scheduling on one store, three days of total silence on another, both with real revenue impact.


Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for UK agencies and businesses. WP-Cron misconfiguration is one of the most common issues I find during onboarding — and one of the easiest to fix permanently. If you're running a WooCommerce store and haven't set up a proper system cron, you're almost certainly losing revenue to missed scheduled events.

Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans → Get in Touch →

Stop Firefighting. Start Maintaining.

I manage 70+ WordPress sites for agencies and businesses. Whether you need ongoing maintenance, emergency support, or a one-off performance fix — I can help.

View Maintenance Plans Get in Touch

Get in Touch to Discuss Your Needs