Every WordPress site relies on a hidden scheduler running quietly in the background. It sends password reset emails, checks for plugin updates, publishes scheduled posts, and runs maintenance tasks, all without you lifting a finger. That scheduler is WP-Cron, and understanding how it actually works is the difference between tasks that run reliably and jobs that silently disappear into the void.
This guide covers everything developers need to know: how WP-Cron is triggered, why it fails on low-traffic sites, how to replace it with a real server cron job, how to register custom intervals, and how to debug missed events with WP-CLI and code. By the end you will have a production-ready scheduling setup you can trust.
How WP-Cron Actually Works
The name “WP-Cron” implies it is a Unix cron daemon. It is not. WP-Cron is a pseudo-cron system, a PHP file called wp-cron.php that lives in your WordPress root directory. Instead of being invoked by the operating system on a fixed schedule, it is triggered by incoming page requests.
Here is exactly what happens every time someone visits your WordPress site:
- WordPress boots and runs
wp-settings.php - Core calls
wp_cron()which is hooked to theinitaction wp_cron()reads thecronoption from thewp_optionstable, a serialized array of scheduled events- It checks whether any event timestamps are in the past
- If overdue events exist, WordPress fires a non-blocking HTTP request to
wp-cron.phpin the background usingwp_remote_post() wp-cron.phpthen executes all hooks whose timestamps have passed
The critical word in step 5 is “non-blocking.” WP-Cron dispatches itself asynchronously, the visitor’s page load is not held up waiting for cron to complete. The cron request runs as a separate HTTP process that the server handles on its own.
The Fundamental Problem
Because WP-Cron only fires when someone visits the site, it has one critical dependency: traffic. A site that receives ten visitors per day might only check its cron queue ten times. If your scheduled event was supposed to fire at 3:00 AM and nobody visited the site until noon, that event ran nine hours late, or not at all if the queue was full or the HTTP request timed out.
For staging environments, development sites, and low-traffic production sites, WP-Cron simply cannot be relied upon for time-sensitive tasks. This is why replacing it with a real server cron job is considered best practice for any site that matters.
Scheduling Events with wp_schedule_event
The primary function for scheduling recurring tasks is wp_schedule_event(). It accepts three parameters: a Unix timestamp for when the event should first run, a recurrence string, and the hook name your callback is attached to.
The snippet below shows the complete lifecycle, schedule on activation, run the task, and clean up on deactivation. Skipping the cleanup step is one of the most common mistakes plugin developers make: orphaned cron entries accumulate in wp_options and slow down every page load.
Checking Before You Schedule
Notice the wp_next_scheduled() check before calling wp_schedule_event(). This guard is mandatory. Without it, every time the plugin is activated, or if the activation hook fires more than once due to a multi-site scenario, you end up with duplicate cron entries for the same hook. Duplicate entries cause your callback to run multiple times per interval, which is especially dangerous for tasks that modify data or send emails.
Single Events vs. Recurring Events
WordPress provides two scheduling functions:
| Function | Use Case | Repeats? |
|---|---|---|
wp_schedule_event() | Regular background tasks (email digests, cache warming, feed imports) | Yes, on every interval |
wp_schedule_single_event() | Deferred one-time actions (sending a follow-up email 24 hours after signup) | No, runs once and is removed |
For wp_schedule_single_event(), the signature is the same except there is no recurrence parameter. WordPress automatically removes the entry from the queue after it fires. You do not need to unschedule it manually on deactivation.
Registering Custom Cron Intervals
Out of the box, WordPress ships with three intervals: hourly, twicedaily, and daily. If your task needs to run every 15 minutes, every 6 hours, or weekly, you must register a custom interval using the cron_schedules filter.
A few things worth noting about custom intervals:
- The
intervalkey is in seconds. WordPress provides time constants,MINUTE_IN_SECONDS,HOUR_IN_SECONDS,DAY_IN_SECONDS,WEEK_IN_SECONDS, that make the math readable and maintainable. - The display string is for humans, not for code. It appears in tools like WP-CLI’s
wp cron schedule listand in plugin dashboards. Make it descriptive. - The filter always receives the existing schedules array. You must return the modified array, forgetting the
returnstatement will silently wipe all existing schedules. - Custom intervals persist as long as the filter is registered. If you deactivate the plugin that adds an interval but leave a scheduled event using that interval, WordPress falls back to running the event on every page load since it cannot calculate the next run time.
Disabling WP-Cron and Using a Real Server Cron
For any production site with predictable traffic patterns, or for any site where timing precision matters, you should disable WordPress’s built-in pseudo-cron and replace it with a system-level cron job. This gives you reliable, time-accurate execution independent of visitor traffic.
Step 1: Set DISABLE_WP_CRON
Adding define( 'DISABLE_WP_CRON', true ); to wp-config.php tells WordPress to skip the non-blocking HTTP request to wp-cron.php on every page load. This removes the performance overhead on high-traffic sites and stops unreliable pseudo-cron behavior on low-traffic ones.
After setting this constant, WP-Cron will not run on its own anymore. You must immediately configure a real server cron job or scheduled events will never execute.
WP-CLI vs Direct PHP Call
There are two common ways to invoke the cron runner from a server cron job. The WP-CLI approach is strongly preferred for most setups:
| Method | Command | Pros | Cons |
|---|---|---|---|
| WP-CLI | wp cron event run --due-now | Bootstraps WordPress properly, respects multisite, no HTTP overhead, easy to debug | WP-CLI must be installed on the server |
| Direct PHP | php /path/to/wp-cron.php | No additional dependencies | May not set correct $_SERVER vars, runs as file system user not web user |
| HTTP curl | curl https://example.com/wp-cron.php | Closest to how WP-Cron normally runs | Requires HTTP stack, adds latency, fails if site is down |
For most Linux/VPS environments, running wp cron event run --due-now --path=/var/www/html --quiet every minute from the server’s crontab is the gold standard. The --quiet flag suppresses output, which prevents crontab from sending email notifications on every execution.
Server Cron Frequency: Every Minute vs. Every 5 Minutes
Running the cron runner every minute ensures the maximum possible accuracy, an event due at 3:00 AM will run no later than 3:00:59 AM. For most sites this is overkill and every 5 minutes is perfectly adequate. The tradeoff is straightforward: more frequent execution means higher accuracy but marginally more server load per hour.
WooCommerce, membership plugins, and anything that processes time-sensitive payments or subscriptions generally warrants every-minute execution. Static content sites, portfolios, and blogs are well served by every 5 minutes.
Debugging Missed Cron Events
Cron bugs are notoriously difficult to spot because they fail silently. The task simply does not run, and unless you are actively monitoring the cron queue, you may not know there is a problem until something downstream breaks, emails stop going out, scheduled posts stay in draft, inventory levels never sync.
Using WP-CLI to Inspect the Cron Queue
WP-CLI is the fastest way to get a complete picture of what is scheduled and whether it is running correctly.
The output of wp cron event list includes the hook name, the next run timestamp (in human-readable format), and the schedule interval. If you see events with a “next run” time that is hours or days in the past, those events are overdue and WP-Cron is not firing them, either because the site has no traffic, because the HTTP request to wp-cron.php is being blocked, or because a PHP error is causing the cron process to abort.
Debugging in the WordPress Admin
For those who prefer a code-based approach, the following admin notice helper gives you a visual cron queue dump in the WordPress dashboard. Drop it in a plugin file, load the admin as an administrator, and you get a full table of every scheduled hook with its next run time and overdue status highlighted.
Remove this code once debugging is complete. Displaying cron queue data in an admin notice is helpful during development but unnecessary, and mildly distracting, on a production site.
Common Root Causes of Missed Events
When events are consistently missed, the cause almost always falls into one of these categories:
- Low or no traffic. WP-Cron cannot fire if nobody visits. The fix is a real server cron job as described above.
- The
wp-cron.phprequest is being blocked. Some hosts and security plugins block direct requests towp-cron.phpfrom127.0.0.1or block the loopback connection entirely. Check your server error logs and security plugin settings. The Wordfence firewall, in particular, has a known issue with blocking internal WP-Cron requests. - PHP fatal error inside the cron callback. If your callback function throws an uncaught exception or fatal error, the entire cron process dies mid-run. Enable
WP_DEBUG_LOGinwp-config.phpand checkwp-content/debug.logfor error messages timestamped around when your events should have fired. - Duplicate events created without the
wp_next_scheduled()guard. Duplicate entries cause the queue to grow indefinitely. You will see the same hook appear dozens of times in the cron array. Use the WP-CLI command or the debug helper above to identify them, then usewp_clear_scheduled_hook()to purge duplicates. - Cron constant or option table issues. If the
cronoption inwp_optionsbecomes corrupted (serialization error, truncated value), the entire cron system stops. This is rare but can happen after a botched migration or a direct database edit. Usewp option get cronvia WP-CLI to inspect the raw value. - Transient lock not expiring. WP-Cron uses a transient called
doing_cronas a lock to prevent concurrent execution. If a cron run dies halfway through without releasing the lock, subsequent runs are blocked until the transient expires (typically 10 minutes). If you suspect a stuck lock, delete it withwp transient delete doing_cron.
WP-Cron on WordPress.com and Managed Hosts
If you are on a managed WordPress host like WordPress.com, WP Engine, Kinsta, or Pressable, the host typically manages cron execution at the infrastructure level. The behavior varies:
- WordPress.com (hosting platform): Runs WP-Cron on a managed schedule. You cannot install arbitrary plugins with cron events on the basic plans.
- WP Engine: Runs
wp-cron.phpvia a server-side process every 5 minutes, independent of traffic. You can still setDISABLE_WP_CRONand configure your own job if you need per-minute precision. - Kinsta: Similar to WP Engine, runs WP-Cron via server cron every 15 minutes. They recommend disabling the built-in pseudo-cron and configuring a custom schedule through their MyKinsta dashboard for time-sensitive tasks.
- Shared Hosting (cPanel): Most cPanel hosts give you access to the “Cron Jobs” section under Advanced. You can add a new cron job there pointing to WP-CLI or directly to
wp-cron.php. The minimum interval available is typically every minute.
Always check your host’s documentation before configuring system cron. On some managed hosts, attempting to run wp-cron.php via a server cron job results in duplicate execution if the host is already running it.
Performance Considerations for High-Traffic Sites
On a site receiving hundreds of concurrent requests per second, the default WP-Cron behavior creates a measurable performance problem. Every single page request triggers a check of the cron queue via wp_cron(). Even though the function returns early if no events are due, the database query to read the cron option still happens on every single request. If you are already working on broader site performance, our WordPress performance optimization guide covers object caching, database query reduction, and server-level tuning that pairs well with a proper cron setup.
Setting DISABLE_WP_CRON eliminates this overhead entirely. Combine it with a server cron job running every minute and you get both better performance and more reliable scheduling, a net improvement on every axis.
Cron Events That Block Page Loads
There is a subtler performance issue worth knowing. If a visitor’s page request triggers a cron check and the cron HTTP request back to wp-cron.php fails to complete asynchronously, which can happen on servers where the loopback connection is slow or unreliable, the page load can be held up waiting for the cron request to time out. This manifests as intermittent slow page loads that are difficult to diagnose in application performance monitoring because the PHP execution time looks normal.
Setting DISABLE_WP_CRON also solves this problem. The loopback HTTP request never happens.
Using Action Scheduler for Heavy Tasks
For high-volume background processing, sending thousands of emails, importing large datasets, processing webhook queues, WP-Cron is not the right tool even with a reliable server cron job. WP-Cron was designed for simple recurring maintenance tasks, not for queue processing at scale.
The Action Scheduler library (used internally by WooCommerce, Gravity Forms, and many other major plugins) is the correct solution for heavy workloads. It stores jobs in a dedicated database table, supports concurrency control, provides a full admin UI for monitoring and retrying failed jobs, and uses WP-Cron only as the runner, not as the queue storage mechanism. If you are processing more than a few hundred background jobs per day, Action Scheduler is worth evaluating.
Security: Preventing Direct wp-cron.php Access
By default, anyone on the internet can directly request https://yoursite.com/wp-cron.php and trigger cron execution. This is not typically a severe vulnerability, WordPress checks an internal lock transient and exits quickly if another cron process is already running, but it does allow external actors to cause repeated cron executions, which wastes server resources and could theoretically be used as a denial-of-service vector against sites with expensive cron callbacks.
If you have disabled the built-in WP-Cron and are using a server-side cron job instead, you can block direct access to wp-cron.php at the server level. For nginx, add this to your server block:
location = /wp-cron.php { deny all; return 403; }
nginx config snippet, add inside your server { } block
For Apache, add a Deny from all directive for that specific file path in your .htaccess or VirtualHost configuration. Alternatively, if your server cron job uses WP-CLI (which boots WordPress directly without an HTTP request), blocking HTTP access to wp-cron.php is safe since WP-CLI never hits that URL.
Quick Reference: WP-Cron Functions
| Function | Purpose |
|---|---|
wp_schedule_event( $timestamp, $recurrence, $hook ) | Schedule a recurring event |
wp_schedule_single_event( $timestamp, $hook ) | Schedule a one-time event |
wp_next_scheduled( $hook ) | Get the next run timestamp for a hook (false if not scheduled) |
wp_unschedule_event( $timestamp, $hook ) | Remove a specific scheduled event |
wp_clear_scheduled_hook( $hook ) | Remove all scheduled events for a hook (useful for cleanup) |
wp_unschedule_hook( $hook ) | Remove all events for a hook across all schedules (WP 5.1+) |
wp_get_schedules() | Get all registered cron schedules |
_get_cron_array() | Get the raw cron queue array from the database |
wp_reschedule_event( $timestamp, $recurrence, $hook ) | Reschedule an event with a new recurrence |
Recommended Plugins for Cron Management
While WP-CLI and code-based debugging are the most reliable approaches, several WordPress plugins provide visual interfaces for managing and monitoring cron events:
- WP Crontrol, The most widely used cron management plugin. Shows all scheduled events, allows you to add, edit, run, and delete events from the admin, and displays all registered schedules. Ideal for debugging on sites where you do not have server access.
- Advanced Cron Manager, Similar to WP Crontrol with a more polished UI. Includes a history view showing recent cron executions and their duration.
- Query Monitor, Not a cron-specific tool, but its “Cron” panel shows all scheduled events and flags any that are overdue. Extremely valuable as a general debugging tool that also covers cron.
For production environments, I recommend against keeping these plugins active permanently. Use them for diagnosis, fix the issue, then deactivate. WP Crontrol in particular adds a small overhead to every admin page load since it queries the cron queue for the dashboard widget. If page speed is a priority alongside task scheduling, also review our roundup of the best WordPress caching plugins, a well-configured cache layer reduces the per-request overhead of WP-Cron on lower-traffic sites.
Putting It All Together: A Production-Ready Setup
Here is the complete checklist for a reliable WordPress cron setup on a VPS or dedicated server:
- Disable the pseudo-cron. Add
define( 'DISABLE_WP_CRON', true );towp-config.php. - Add a server cron job. Run
* * * * * wp cron event run --due-now --path=/var/www/html --quietviacrontab -eas the web server user (typicallywww-dataon Ubuntu/Debian). - Verify WP-CLI is in the PATH for the cron user. Test with
sudo -u www-data wp --info. - Block direct access to wp-cron.php at the nginx or Apache level.
- Use
wp_next_scheduled()guards in all plugin activation hooks to prevent duplicate scheduling. - Always unschedule on deactivation using
wp_clear_scheduled_hook(). - Register custom intervals early (on the
cron_schedulesfilter, not inside a hook that might fire late). - Log cron callback failures by wrapping your callback logic in a try/catch and logging exceptions to
error_log()or a custom log table.
Following this checklist gives you a cron system that runs reliably, does not impact page load performance, is protected from external abuse, and is easy to debug when something goes wrong.
Frequently Asked Questions
Can WP-Cron run while the site is in maintenance mode?
No. When WordPress is in maintenance mode (the .maintenance file is present in the root), all HTTP requests, including the internal WP-Cron request, return a 503 response. If you are using a server cron job with WP-CLI, cron execution continues normally because WP-CLI bypasses the HTTP layer entirely. This is another reason to prefer WP-CLI over direct PHP or curl for server-side cron invocation.
Does setting DISABLE_WP_CRON break scheduled posts?
Yes, if you do not configure a server cron job as a replacement. The publish_future_post hook that publishes scheduled posts is a cron event. With DISABLE_WP_CRON set and no server cron replacement, scheduled posts will remain in the “Scheduled” state indefinitely and never publish. Always set up the server cron job immediately after disabling the built-in pseudo-cron.
How do I run a cron event immediately for testing?
Use WP-CLI: wp cron event run your_hook_name. This runs the hook immediately regardless of its scheduled time. You can also use the WP Crontrol plugin to manually trigger an event from the admin, click “Run now” next to any event in the cron event list.
Can I schedule a cron event to fire at a specific time of day?
Yes. Pass the desired Unix timestamp as the first argument to wp_schedule_event(). For example, to schedule a daily event that first fires at midnight UTC: wp_schedule_event( strtotime( 'midnight' ), 'daily', 'myplugin_midnight_task' );. WordPress will then run the event every 24 hours from that initial timestamp. Note that if the initial timestamp has already passed when the event is registered, the first execution will happen on the next cron check.
Final Thoughts
WP-Cron is one of those WordPress systems that works well enough to be invisible until it does not, and when it fails, the failures are quiet enough that you might not notice for days. The good news is that fixing it properly takes about fifteen minutes: disable the pseudo-cron, add a real server cron job, and add a few defensive checks to your plugin code. After that, you have a scheduling system you can actually rely on.
If you are building anything that depends on time-sensitive background processing, automated emails, subscription renewals, data sync jobs, feed imports, treat cron reliability as a first-class concern from the start. The cost of getting it right is low. The cost of debugging mysteriously missed events in production is much higher.
Build Better WordPress Sites
If you are serious about WordPress development, understanding the internals, like how WP-Cron actually works, is what separates reliable production sites from brittle ones. WPPioneer covers advanced WordPress development topics every week. Explore more in-depth tutorials in our For Developers archive, or check out our guide on the complete WordPress performance optimization guide for the next deep dive.
Scheduled Tasks WordPress Automation WordPress Cron Jobs WordPress Performance WP-Cron
Last modified: April 9, 2026









