WordPress makes scheduled background work look simple: choose a hook, choose an interval, and call wp_schedule_event(). The difficult part is not creating an event. It is making sure the event is scheduled once, runs when you expect it to, can safely run again after a failure, and disappears when the code that owns it is removed.
This guide explains the practical decisions behind WP-Cron rather than treating it as a copy-and-paste API. If you need a starting snippet while reading, use the WordPress Cron Generator. It creates recurring, one-time and custom-interval examples locally in your browser.
What WP-Cron actually is
WP-Cron is WordPress’s task scheduling system. It is not the same thing as the operating system’s cron daemon. WordPress stores scheduled events and checks whether work is due while WordPress is running. A due event can therefore execute after its nominal scheduled time rather than at an exact second.
This distinction matters. A daily cleanup task that runs a few minutes late may be completely acceptable. A workflow that must fire at exactly 09:00, regardless of traffic, needs a more deliberate scheduling architecture.
The WordPress Plugin Handbook describes WP-Cron as an interval-based scheduler. Core supplies hourly, twice-daily, daily and weekly recurring intervals. Developers can add other intervals with the cron_schedules filter.
The three pieces of a scheduled WordPress task
A reliable WP-Cron implementation is easier to reason about when you separate three concerns.
1. The event hook
The hook is the name WordPress stores in the schedule. Your code connects that hook to a callback with add_action(). Use a unique prefix so another plugin is unlikely to use the same hook.
add_action( 'acme_sync_inventory', 'acme_run_inventory_sync' );
function acme_run_inventory_sync() {
// Background work.
}
2. The schedule
The schedule says when the first event becomes due and, for recurring events, which recurrence WordPress should use afterward. The recurrence is a schedule key such as hourly or daily, not an arbitrary number of seconds passed directly to wp_schedule_event().
3. The callback
The callback does the actual work. This is where many cron implementations become fragile. A perfect schedule cannot rescue a callback that times out, processes an unlimited queue, creates duplicates when retried, or assumes an external API always responds successfully.
Why wp_next_scheduled() is so important
WordPress documentation explicitly warns that calling wp_schedule_event() repeatedly can schedule the same task multiple times. That is easy to do if scheduling code runs on normal requests.
A basic recurring pattern therefore checks first:
if ( ! wp_next_scheduled( 'acme_sync_inventory' ) ) {
wp_schedule_event( time(), 'hourly', 'acme_sync_inventory' );
}
The guard is not decorative. Without it, a frequently executed code path can keep adding events. The problem becomes more subtle when events have arguments because WordPress uses the hook plus arguments to identify scheduled events. Your check, scheduling call and cleanup logic need to agree on those arguments.
If a job appears to run two, five or fifty times when it should run once, inspect the schedule before assuming the callback itself is looping.
Recurring events versus one-time events
Use wp_schedule_event() for work that should repeat. Use wp_schedule_single_event() for work that should happen once in the future.
One-time events are useful for delayed follow-up work: retrying a failed operation later, expiring a temporary state, or deferring a non-urgent action until after the current request.
Recurring events suit maintenance and synchronization: checking a feed, processing a queue, cleaning old records or recalculating data periodically.
Do not turn every delayed task into a permanent recurrence. If the business requirement is “try this once in ten minutes,” a single event expresses that requirement more clearly.
Adding a custom WP-Cron interval
If hourly is too slow and you need, for example, every 15 minutes, register a schedule first:
add_filter( 'cron_schedules', 'acme_cron_schedules' );
function acme_cron_schedules( $schedules ) {
$schedules['acme_every_15_minutes'] = array(
'interval' => 15 * MINUTE_IN_SECONDS,
'display' => 'Every 15 minutes',
);
return $schedules;
}
You can then use acme_every_15_minutes as the recurrence passed to wp_schedule_event().
There is an important design question before you add a very short interval: does the job actually need to run that often? If one run can take longer than the interval, or a remote service has rate limits, increasing frequency can make reliability worse. Consider smaller batches, explicit locks, queue processing or a purpose-built background processing system for heavier workloads.
First-run time is different from recurrence
The timestamp passed to wp_schedule_event() controls when the first event is due. The recurrence controls the interval after that. These are separate concepts.
If you want a job to start five minutes from now and then repeat hourly, you can calculate the initial timestamp from the current time. If you need “every day around 9 AM in the site’s timezone,” timezone handling deserves extra care. Do not casually mix server time, browser time and the WordPress site timezone.
For operational code, test the resulting next-run timestamp on a staging site instead of assuming a human-readable time calculation resolved as intended.
Clean up recurring events when code is deactivated
A plugin should not leave obsolete scheduled work behind. WordPress’s Plugin Handbook specifically recommends unscheduling tasks when they are no longer needed and highlights plugin deactivation as an important cleanup point.
A simple plugin-owned hook can be cleared on deactivation:
register_deactivation_hook( __FILE__, 'acme_deactivate' );
function acme_deactivate() {
wp_clear_scheduled_hook( 'acme_sync_inventory' );
}
If your event uses arguments, make sure cleanup targets the same event identity. Treat lifecycle cleanup as part of the feature, not an optional polish step.
Why a cron event can be late
WP-Cron does not behave like a daemon continuously watching the clock. WordPress’s scheduling documentation notes that a scheduled hook is triggered when WordPress receives a visit after the scheduled time has passed. On a low-traffic site, that can mean a job is late.
For ordinary housekeeping this is often fine. For a store integration that must process time-sensitive data predictably, consider a real server scheduler that invokes WordPress cron. If you take that approach, avoid accidentally running both mechanisms in a way that creates unnecessary cron spawning.
The WordPress wp-config.php Generator can help you prepare a reviewed DISABLE_WP_CRON setting, but disabling normal WP-Cron only makes sense after an external scheduler is actually configured.
Design the callback to be safe to retry
Scheduling is only half the reliability problem. Background callbacks should be designed with retries and partial failures in mind.
Imagine a cron callback that imports 1,000 products from an external service. It updates 600 products and then the request fails. On the next run, does it safely continue, update the same records idempotently, or create duplicates?
A more robust design usually processes bounded batches, stores progress where appropriate, validates remote responses, logs actionable failures, and makes repeated processing of the same item safe whenever possible.
Also consider overlap. If a task can take 20 minutes but is scheduled every 10 minutes, a second run may start while the first is still working. A lock or queue architecture may be necessary. Do not use a short interval as a substitute for thinking about concurrency.
How to verify a WP-Cron implementation
Do not stop after seeing that an event exists. Verification should answer several different questions.
- Is the expected hook scheduled? Check the event list and next-run time.
- Is it scheduled only once? Look for duplicate instances, especially after multiple requests or plugin updates.
- Is the callback registered? A scheduled hook with no useful callback does no useful work.
- Does the callback complete? Verify the business result: the feed changed, the queue shrank, the record was updated, or the cleanup happened.
- Does failure leave useful evidence? Check application logs and any task-specific logging.
- Does deactivation clean it up? Test the lifecycle on staging.
WP-CLI also provides cron commands for inspecting schedules and events. For debugging PHP errors from callbacks, the WordPress Debug Log Analyzer can group repeated failures and highlight likely plugin or theme paths.
Common WP-Cron mistakes
Scheduling on every request without a guard. This creates duplicate events instead of making the existing event “more reliable.”
Assuming the interval is an exact clock. WP-Cron’s execution model allows lateness.
Using an unsupported recurrence name. If you need a custom interval, register it before scheduling the event.
Leaving events after deactivation. Old hooks can remain scheduled even when the feature that created them is gone.
Putting unlimited work in one callback. Large imports, API synchronization and email queues need batching and failure handling.
Testing only the schedule. An event can exist while the callback fails every time.
When WP-Cron is the right choice
WP-Cron is a good fit for many WordPress tasks because it is built into the platform and integrates naturally with plugin hooks. Use it when timing can tolerate some delay, the callback is reasonably bounded, and the site receives enough activity—or an external trigger is available—to service due work.
For strict timing, high-volume queues or jobs requiring stronger concurrency guarantees, use an architecture designed for those constraints. The goal is not to avoid WP-Cron; it is to use it for the class of work it handles well.
A practical implementation checklist
- Give hooks and callbacks a unique project/plugin prefix.
- Use
wp_next_scheduled()before creating recurring events. - Use a built-in recurrence unless a custom interval is genuinely needed.
- Keep the callback bounded and safe to retry.
- Think about overlapping runs before choosing a short interval.
- Unschedule plugin-owned recurring events on deactivation.
- Verify the actual business result, not merely the existence of the cron entry.
- Use a real scheduler when execution timing needs to be more predictable.
If you want to turn these decisions into a starting snippet, open the WordPress Cron Generator, choose recurring or one-time mode, and review the generated PHP before adding it to your codebase.

