WooCommerce’s Sale Scheduler Only Works When Someone Visits Your Store
WooCommerce Infrastructure Guide
Your WooCommerce Scheduler Has a Hidden Catch
You scheduled your Black Friday sale to start at midnight. What actually started it β and when it actually started β depends on something most store owners have never heard of.
[LAST UPDATED: March 2026]
Picture this: you’ve set up a flash sale to run over a weekend. You scheduled it to start Friday at 6pm and end Sunday at midnight. You went to the effort of setting up the dates, checked it in preview, and then left it to run automatically β which is the whole point of scheduling.
Monday morning you check the store. The sale prices are gone. But when you look at the order timestamps, the last sale-price orders came in at 3am Sunday, not midnight. And more interesting β a few customers who visited the store at 9am Sunday told you they still saw the sale prices well into the morning.
Nothing is visibly broken. WooCommerce shows no errors. The sale is now correctly ended. But it didn’t end when you told it to. It ended when it felt like it β specifically, when enough page visits happened to trigger a background process you’ve never had to think about before.
That background process is called WP-Cron. It is the scheduler that every time-sensitive WooCommerce operation depends on. And it works in a way that is genuinely surprising when you understand it properly.
What WP-Cron actually is (it’s not what you think)
Most people, when they hear the word “scheduler,” picture something like an alarm clock. You set the time, and when that time arrives, the alarm fires β independent of anything else happening in the world. The clock doesn’t need someone to walk past it to ring.
WP-Cron is not like this. It’s closer to an alarm clock that only rings if someone happens to walk into the room.
WordPress has no persistent background process. It is not running continuously on your server between visitor requests. When no one is loading a page, WordPress is, in a meaningful sense, asleep. There is no daemon sitting there checking the clock, waiting for the right moment to trigger your scheduled sale. The only thing running on your server is whatever your hosting company runs by default β and that’s not WordPress.
WP-Cron is a clever workaround for this reality. When any visitor loads any page on your WordPress site, the platform seizes that moment to check a queue of pending scheduled tasks. If anything is overdue, it fires it. The page load that triggered the check is essentially a bystander β the visitor gets their page, and in the background, WordPress uses that incoming request as an opportunity to do its scheduled work.
The consequence is direct: if nobody visits your site between the scheduled time and some later point, the scheduled task simply waits. It doesn’t run. It doesn’t fail. It doesn’t notify you. It just waits for the next visitor to walk through the door and unwittingly give WordPress permission to proceed.
Why WordPress was designed this way
This isn’t a bug or an oversight β it’s a deliberate architectural decision made to accommodate how WordPress is actually deployed. The vast majority of WordPress installations live on shared hosting: cheap, reliable, and completely inaccessible via SSH or shell. On shared hosting, you cannot configure real system-level cron jobs. You don’t have root access. You can’t schedule a server-level task. WP-Cron was built specifically to solve this problem β to provide scheduled-task capability to the millions of WordPress sites on shared hosting where no other option existed. It was an elegant solution to a real constraint. The tradeoff is that “scheduled” means “as soon as the next visitor arrives after the scheduled time,” not “at exactly the scheduled time.”
The mechanism: loopback requests and the page-load trigger
The internal mechanics are worth understanding, because they explain both the normal behaviour and the failure modes.
When a page loads on your WordPress site, one of the early checks WordPress makes is whether any scheduled tasks are due. It does this by looking at a queue stored in the database β specifically in a WordPress option that holds a list of pending tasks with their scheduled timestamps. If the current time is past any of those timestamps, those tasks are overdue and need to run.
If overdue tasks are found, WordPress doesn’t run them directly as part of the page request β that would slow down the page load for the visitor who triggered the check. Instead, it initiates what’s called a loopback request: a non-blocking HTTP call from your server back to itself. Specifically, it calls wp-cron.php β a file in your WordPress root that processes the scheduled task queue. This loopback request runs asynchronously, so the original visitor’s page loads normally while the background task runs in parallel.
The loopback approach is smart. From the visitor’s perspective, the page loads at normal speed. In the background, your scheduled tasks run. Nobody waits. But it introduces a dependency that most store owners never consider: the loopback request is an HTTP request from your server to your server. If anything blocks that request β a security plugin, a firewall, a misconfigured server, an SSL certificate issue β the loopback fails, and the scheduled task never runs. No error is logged at the application level. The task just stays in the queue.
The timing gap in practice
Even when everything is working correctly, there is always a gap between the scheduled time and the actual execution time. The gap is equal to the time between the scheduled moment and the next page load on your site.
For a busy store with consistent traffic throughout the day, this gap is usually small β seconds to minutes. For a store that’s quiet at night, it can be hours. If you schedule a flash sale to start at midnight and your last visitor left at 11:45pm with the next one arriving at 7:30am, your flash sale will start at 7:30am. The sale price was scheduled for midnight. Nobody visited the site in those seven-plus hours. There was nothing to trigger the cron. So it waited.
This is not a catastrophic failure. The task doesn’t disappear β it runs the moment traffic resumes. But it does mean that precision timing is not a property of the default WP-Cron system. “Scheduled for midnight” means “will run sometime around midnight, depending on traffic.”
Every time-sensitive WooCommerce operation that depends on this
WP-Cron is the foundation for every automated, time-sensitive operation in a standard WooCommerce installation. Once you understand how it works, the list of things it controls starts to feel uncomfortably long.
Scheduled sale prices
When you set a sale price with start and end dates in WooCommerce β either directly on a product or via a third-party campaign tool β the activation and deactivation of those prices are handled by scheduled WP-Cron events. The event is queued at the scheduled time. It fires when the next page load occurs. Until that page load happens, the price doesn’t change.
This means a product scheduled to go on sale at 9am might not show the sale price until 9:17am, if the first visitor of the morning arrives at 9:17. And a product scheduled to end its sale at 6pm might still show sale prices at 6:45pm if traffic is slow. Most of the time, on a reasonably active store, this drift is harmless. But during a high-stakes promotion β a flash sale with a genuine time limit, a price match that expires at a specific hour β the gap between scheduled and actual can matter.
WooCommerce Subscriptions renewal processing
Subscription payment renewals are scheduled events. When a subscription is due to renew on the 15th of the month at 3am, that renewal is queued as a scheduled task β ultimately relying on WP-Cron to initiate it. If traffic is low at 3am (and it usually is), renewal processing is delayed until the first visitor of the morning triggers the cron.
In practice this is usually fine, because payment gateways don’t care whether the charge comes at 3am or 6am. But in edge cases β a subscription whose renewal needs to happen before a certain hour to avoid a service interruption, or a store where the subscription system is already strained β the timing dependency is real.
Abandoned cart recovery emails
Plugins that send “you left something in your cart” emails after a defined interval β typically 1 hour, 24 hours, or similar β schedule those sends via WP-Cron. The email isn’t sent on a real clock; it’s sent when the next page load occurs after the interval expires. If your store is quiet in the early hours and your cart recovery is set to fire 1 hour after abandonment, a cart abandoned at midnight might not trigger its recovery email until the next morning’s traffic. The window where that email would be most effective β immediately after abandonment β may have long since passed.
Stock level updates and inventory checks
Background inventory processes, low-stock notifications, and automatic stock restoration after failed orders are all scheduled operations. They run on the same WP-Cron system. In most cases, a delay of minutes or even hours in these processes doesn’t cause a customer-facing problem. But in high-velocity scenarios β a flash sale where stock is depleting rapidly β a delay in the scheduled stock check can mean overselling a product that ran out an hour ago.
Email notifications, reports, and cleanup tasks
WooCommerce sends a range of scheduled emails: admin order reports, low-stock alerts, customer follow-ups. Scheduled database cleanup β clearing expired sessions, cleaning up old transients β runs on WP-Cron. WordPress core itself uses WP-Cron for update checks, spam comment cleanup, and other maintenance tasks. None of these are real-time. All of them drift based on traffic.
WooCommerce Action Scheduler: what it adds, and what it doesn’t fix
If you look at your WooCommerce dashboard under WooCommerce β Status β Scheduled Actions (or a similar path depending on your version), you’ll encounter something called the Action Scheduler. It’s a background job queue library that WooCommerce bundles and uses for its own scheduled tasks β and it’s important to understand what it is and, crucially, what it isn’t.
Action Scheduler is a layer built on top of WP-Cron. It provides a much more sophisticated queueing system than WP-Cron’s basic event list. Where WP-Cron stores its queue in a WordPress option (a flat list in the database), Action Scheduler maintains a dedicated database table with full logging, status tracking, retry logic, failure handling, and an admin interface that lets you see what ran, when it ran, and what failed.
These are genuine improvements. If a WooCommerce task fails partway through β an Action Scheduler job throws an exception, for example β it gets marked as failed and you can see it. That’s much better than WP-Cron, where a failure often just means the task stops silently. Action Scheduler also supports batch processing, so large queues of tasks (like processing 500 subscription renewals at once) can be distributed across multiple runs rather than blocking a single cron execution.
But here is the critical thing: Action Scheduler does not change the fundamental trigger mechanism.
Action Scheduler still relies on WP-Cron to initiate its processing loop. It hooks into a WP-Cron event called action_scheduler_run_schedule β and that event is itself a scheduled WP-Cron event. When that WP-Cron event fires (triggered by a page load, same as always), Action Scheduler wakes up and processes its queue. If WP-Cron doesn’t fire β because there’s no traffic, or because the loopback is blocked β Action Scheduler’s queue doesn’t drain either.
The addition of Action Scheduler gives you better visibility and better resilience for the tasks themselves. It does not give you true time independence. The alarm clock still only rings if someone walks into the room.
What “Failed Actions” in Action Scheduler usually means
If you see a count of failed actions in your WooCommerce Status screen, it does not always mean the tasks failed because WP-Cron didn’t fire. Failed actions usually represent tasks that ran but threw a PHP error or exception during execution. These are a different problem β typically a plugin conflict, a database issue, or a task that received bad data. A growing queue of “pending” actions that aren’t draining is the signal that WP-Cron itself may not be running reliably. These are different symptoms with different causes.
The four conditions that cause WP-Cron to fail silently
WP-Cron “failing” is rarely a clean error. In most cases it simply doesn’t run, and no notification goes out. Here are the four most common situations where this happens.
1. The loopback request is blocked
When WordPress tries to initiate a loopback HTTP request to wp-cron.php, that request goes from your server back to your server. Security plugins β particularly those that implement application-level firewalls or IP-rate limiting β can block this self-request because it doesn’t look like a normal visitor. Firewalls that block HTTP requests to wp-cron.php directly, as a “security measure,” will break WP-Cron completely. Some server configurations with strict SSL enforcement block HTTP loopback requests if the connection uses HTTP internally, even if your public site is HTTPS. The result is silent: WP-Cron attempts the loopback, it fails, and nothing in the user interface tells you.
2. No traffic at the scheduled time
The straightforward one. A store that goes quiet overnight β which most stores do β has no WP-Cron execution for hours at a time. Any task scheduled during low-traffic hours will run whenever the first visitor of the morning arrives, not at the scheduled time. For most background maintenance tasks, this is irrelevant. For time-sensitive promotions or communication sequences, the delay can matter.
3. ALTERNATE_WP_CRON changes the behaviour unexpectedly
WordPress has a configuration constant called ALTERNATE_WP_CRON. When this is enabled (by adding define( 'ALTERNATE_WP_CRON', true ); to wp-config.php), the loopback mechanism is replaced with a different approach: instead of spawning a parallel loopback request, WordPress redirects the current visitor away, runs the cron tasks, and then redirects them back. This avoids the loopback-request-blocking problem, but it creates a momentary experience interruption for the visitor who happened to trigger the cron. More importantly, ALTERNATE_WP_CRON is sometimes added to wp-config.php by hosting providers or setup wizards without the store owner knowing, which changes the timing behaviour in ways that aren’t obvious from the WooCommerce interface.
4. DISABLE_WP_CRON is set β but no real cron was configured to replace it
The recommended solution for WP-Cron’s limitations is to disable it and replace it with a genuine system-level cron job. We’ll cover this properly in a moment. The problem occurs when someone β a developer, a performance plugin, a hosting migration β adds define( 'DISABLE_WP_CRON', true ); to wp-config.php to disable the page-load-triggered system, but then fails to configure the server cron replacement. The result: WP-Cron is completely disabled. No cron runs at all. Scheduled sales never start or end. Subscription renewals don’t process. Emails don’t send. And because nothing fails visibly, it can take days to notice.
The performance tradeoff that’s rarely mentioned
The loopback mechanism has a performance cost. On every page load, WordPress checks whether any scheduled tasks are due β and if they are, it spawns an additional HTTP request to your server to process them. On a high-traffic store, this means cron is running constantly, and the additional PHP process spawned by the loopback request competes with legitimate visitor requests for server resources. This is why Kinsta, WP Engine, and other managed WordPress hosts disable WP-Cron’s page-load trigger and replace it with a genuine server cron β not just for reliability, but because separating cron execution from visitor requests improves the performance of both.
How to check whether WP-Cron is healthy on your store right now
Before changing anything, it’s worth finding out what state your store is actually in. There are a few ways to check.
Check your wp-config.php for cron constants
Open your wp-config.php file (via your hosting file manager, FTP, or SSH if you have it) and search for three strings: DISABLE_WP_CRON, ALTERNATE_WP_CRON, and wp-cron. If you see define( 'DISABLE_WP_CRON', true );, your page-load cron is disabled β which is fine if you have a real server cron configured, but a problem if you don’t. If you see nothing about cron, the default behaviour is in effect.
Use the WP Crontrol plugin
WP Crontrol (available free from the WordPress plugin directory) adds a view under Tools β Cron Events that shows your full scheduled task queue: what tasks are pending, when they’re due, and when they last ran. More importantly, it includes a “Check WP-Cron” function that actually tests whether the loopback mechanism is working on your server. If WP Crontrol reports that the loopback is failing, you have confirmed the first failure mode described above, and you know what to fix.
Look at your Action Scheduler queue
In your WordPress admin, navigate to WooCommerce β Status β Scheduled Actions. Look at the “Pending” count. A large and growing pending queue, combined with tasks that were due hours ago and haven’t run, is a signal that your cron is not firing reliably. A modest pending queue with recent run timestamps is normal β it just means there are tasks queued and running as expected.
Check for this in WooCommerce β Status β System Status
WooCommerce’s system status report (WooCommerce β Status β System Status) includes a section on scheduled tasks. It notes whether there are overdue actions and flags if the cron appears to have not run recently. This is a quick first check β not exhaustive, but a useful starting point.
The real fix: replacing WP-Cron with a genuine server cron job
A genuine server cron job is a task scheduled at the operating system level β independent of WordPress, independent of web traffic, running on a real clock that fires at the exact time you specify regardless of what’s happening on your site. This is what WP-Cron was designed to approximate on shared hosting. If you have access to configure one, it’s strictly better.
The setup has two parts.
Part 1: Disable WP-Cron’s page-load trigger
Open your wp-config.php file and add the following line, ideally near the top of the custom configuration section:
define( 'DISABLE_WP_CRON', true );
This tells WordPress to stop triggering cron on every page load. The scheduled task queue still exists. Tasks still get queued and stored. They just won’t fire automatically from page loads anymore β you’re taking over that responsibility with a real server cron.
Part 2: Set up the server cron job
How you do this depends on how your hosting is configured.
If you have cPanel access (common on shared and semi-managed hosting): look for “Cron Jobs” in your cPanel dashboard. Add a new cron job with an interval of every 5 minutes, running this command:
wget -q -O - https://yoursite.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1
Replace yoursite.com with your actual domain. The -q -O - flags tell wget to run silently, discard its output, and not create any files. The >/dev/null 2>&1 suppresses any error output as well β this keeps your cPanel email inbox clean.
Alternatively, if you have PHP available via the command line on your server, this is the more reliable form:
php /path/to/your/wordpress/wp-cron.php
If you have SSH access and can edit the crontab directly: run crontab -e and add a line like this:
*/5 * * * * php /var/www/html/wp-cron.php > /dev/null 2>&1
The */5 * * * * syntax means “every 5 minutes, every hour, every day.” Adjust the path to match your WordPress installation’s actual path on the server.
With this setup, your scheduled tasks now fire within 5 minutes of their scheduled time β every time, regardless of traffic, regardless of whether anyone has visited your site. A flash sale scheduled for midnight starts within 5 minutes of midnight. A subscription renewal due at 3am processes at 3am. The timing gap that WP-Cron introduced is, for all practical purposes, gone.
Why every 5 minutes is the right interval
Running your server cron every minute gives you the tightest timing but generates unnecessary server load β most of the time there’s nothing to process. Every 15 minutes is what many managed hosts use as a default; it’s fine for most tasks but introduces potential drift for time-sensitive flash sales. Every 5 minutes is the practical sweet spot: tight enough that time-sensitive operations fire within an acceptable window, not so frequent that you’re generating constant server overhead. If your store runs truly time-critical operations β countdowns to a specific second, or promotional terms that depend on precise start times β every minute is defensible. For most stores, every 5 minutes is more than sufficient.
What managed WordPress hosts do differently
If your store runs on Kinsta, WP Engine, Flywheel, or similar managed WordPress hosting, there’s a good chance this problem is already handled for you β though not necessarily in the way you’d expect.
Managed hosts typically disable WP-Cron’s page-load trigger and replace it with a server-level cron that calls wp-cron.php on a defined schedule β commonly every 15 minutes, though this varies by provider. Kinsta, for example, explicitly states that their server-side cron triggers wp-cron.php every 15 minutes by default. This prevents the performance overhead of cron running on every page load, and it gives you reliable execution independent of traffic patterns.
However, “every 15 minutes” still means your midnight flash sale might not start until 12:14am β the next cron run after midnight. For most stores, that’s completely acceptable. For a store running a strictly time-limited promotion with countdown timers pointing to a specific minute, it’s worth knowing that even on managed hosting, you’re working within a window, not on a point.
If you’re on managed hosting and want to tighten the interval β or verify exactly how your host handles WP-Cron β check your host’s documentation or open a support ticket asking specifically: “Has WP-Cron’s page-load trigger been disabled, and how often does the server cron replacement run?” Most managed hosts are straightforward about this.
If you’re on shared hosting without a managed service, there is no guarantee that anything has been done about WP-Cron’s default behaviour. The setup described above is something you need to configure yourself β or ask your hosting provider whether they offer a cron scheduling tool (most shared hosts do, typically via cPanel).
Keeping this in perspective β most stores are fine, most of the time
If you’ve read this far and you’re now worried about your store, it’s worth a moment of grounding.
WP-Cron’s architecture has been the default for every WordPress installation since 2008. The majority of WooCommerce stores operating today have never touched their WP-Cron configuration, and most of them work reliably. Subscription renewals go through. Scheduled sales start and end. Cleanup tasks run. The system is not broken by default β it’s just not as precise as the word “scheduled” implies.
The scenarios where WP-Cron’s limitations actually cause a real problem are specific:
- Stores with very low traffic, particularly overnight, where the drift between scheduled time and actual execution can be hours
- Stores running promotions where the start or end time is communicated to customers (a countdown timer, a stated deadline) and the drift would be visible and embarrassing
- Stores where someone has set
DISABLE_WP_CRONwithout configuring the replacement - Stores on hosting environments where the loopback request is blocked by a security plugin or server configuration
If none of those describe your situation, your scheduled operations are probably running within acceptable time tolerances already. The point of understanding WP-Cron isn’t to trigger a panic β it’s to give you an accurate mental model of the system so that when something does go wrong, you know where to look.
The silent nature of WP-Cron failures is the real concern. A flash sale that didn’t start at midnight and started at 8am instead won’t generate any errors in your dashboard. WooCommerce won’t email you. No alarm will sound. The system just quietly adjusts. Understanding the mechanism means you’ll know to check the cron when a scheduled operation appears to have run late β rather than spending an hour debugging the wrong thing.
What this means for campaign scheduling tools
Any WooCommerce plugin that schedules campaigns β discount automations, promotional campaigns, sale price activations β operates on this same infrastructure. Smart Cycle Discounts, for example, schedules campaign start and end events through WordPress’s scheduling system. This means SCD campaigns are subject to the same timing behaviour as everything else built on WP-Cron: they activate and deactivate reliably, but the precision of the timing depends on traffic and server cron configuration. On a store with steady traffic or a properly configured server cron, campaigns fire within seconds to minutes of their scheduled time. On a low-traffic store with default WP-Cron, they fire when the next visitor arrives. The fix is the same for all of it: set up the server cron replacement, and every scheduled operation on your store becomes genuinely time-reliable.
Frequently asked questions
Why did my WooCommerce sale price not start at the scheduled time?
WooCommerce’s scheduled sale prices depend on WP-Cron, which only runs when a visitor loads a page on your site. If no one visited your store between the scheduled start time and when the sale actually activated, the sale was simply waiting for the next page load to trigger the cron process. The fix is to configure a real server-level cron job to call wp-cron.php on a fixed interval (every 5 minutes is typical), so activation happens regardless of traffic. See the server cron setup section above.
Does WooCommerce Action Scheduler fix the WP-Cron reliability problem?
Not entirely. Action Scheduler adds much better logging, failure handling, retry logic, and a visible admin interface for scheduled tasks β all of which are genuine improvements over raw WP-Cron. But Action Scheduler still depends on WP-Cron to initiate its own processing loop. If WP-Cron doesn’t fire (because of low traffic, a blocked loopback, or a misconfigured server), Action Scheduler’s queue doesn’t drain either. The visibility improvements mean you’re more likely to notice a problem, but the underlying trigger dependency is unchanged.
What happens if DISABLE_WP_CRON is set but I haven’t configured a real cron job?
All scheduled operations stop entirely. Scheduled sales won’t start or end. Subscription renewals won’t process. Email notifications won’t send. Background cleanup tasks won’t run. No errors are raised at the application level β tasks simply accumulate in the queue without executing. This is one of the more dangerous configurations because it fails completely and silently. If you find DISABLE_WP_CRON set in your wp-config.php, either configure the server cron replacement immediately or remove the constant to re-enable the page-load trigger.
How do I check if my loopback request is being blocked?
Install the free WP Crontrol plugin (WordPress.org plugin directory). It includes a loopback test that tells you whether WordPress can successfully make the internal HTTP call required to run WP-Cron. If the test fails, the most common causes are a security plugin blocking the request, a server-level firewall rule, or an SSL/HTTP mismatch on the loopback. WP Crontrol’s test result will point you in the right direction.
Does managed WordPress hosting (Kinsta, WP Engine, Flywheel) fix this automatically?
Managed hosts typically disable WP-Cron’s page-load trigger and replace it with a server-level cron that runs on a fixed interval β commonly every 15 minutes. This solves the traffic-dependency problem, but it means tasks still have up to a 15-minute window of timing drift rather than firing at the exact scheduled second. For most stores this is entirely acceptable. If you’re on managed hosting and want to verify how frequently the cron runs, ask your host support team directly β the interval varies by provider and sometimes by plan.
Will setting up a server cron job break anything?
No, if done correctly. The server cron calls the same wp-cron.php file that the page-load trigger calls β the task processing logic is identical. The only change is what initiates the call. The standard approach is to add define( 'DISABLE_WP_CRON', true ); to wp-config.php first, then configure the server cron. This ensures you’re not triggering cron on every page load (performance improvement) while also ensuring the server cron is reliably handling execution. Just make sure the server cron is verified to be running before you disable the page-load version.
What’s the difference between a “pending” action and a “failed” action in Action Scheduler?
A pending action is in the queue, waiting to run β this is normal. Actions become pending when they’re scheduled and move to complete (or failed) once they run. A large and growing count of pending actions that are overdue by hours suggests WP-Cron isn’t firing reliably. A failed action means the task ran but threw an error during execution β typically a plugin conflict, a database problem, or bad data. These are different problems: mounting pending actions point to a cron trigger issue; failed actions point to a code or data issue in the task itself.
The one thing to take away from this
WP-Cron is not a clock. It is a queue that runs whenever someone visits your site. For low-traffic stores, stores running time-sensitive promotions, or stores on hosting environments with blocked loopbacks, the gap between “scheduled” and “actually happened” can matter. The fix is straightforward β a 5-minute server cron job β and it improves timing precision, reduces per-page-load overhead, and makes every scheduled operation on your site more reliable in one configuration change. If you’ve never thought about WP-Cron before, now is a good time to check that it’s running the way you expect.