
How to use WordPress cron jobs?
WordPress cron jobs are scheduled tasks that WordPress runs automatically, such as publishing scheduled posts, checking for updates, deleting old trash, and sending plugin emails. WordPress handles them with its own system, called WP-Cron, which you use by registering an event with functions like wp_schedule_event() and attaching your code to a hook. For reliable timing, you can also disable the built-in trigger and let a real server cron job run WP-Cron on a fixed schedule.
Cron is one of those features you don't notice until it stops working, usually when a scheduled post shows "Missed schedule" or a backup plugin quietly stops running. This article explains how WP-Cron works under the hood, how to schedule your own tasks in PHP, how to view and manage events from the dashboard and WP-CLI, and how to set up a proper server cron job for production sites.
What Is a Cron Job?
On Linux and Unix servers, cron is a time-based job scheduler. You write an entry in a crontab file that says "run this command every 15 minutes" or "run this script at 3:00 AM every day," and the operating system's cron daemon runs it at exactly that time.
WordPress can't assume every host gives you access to the system crontab, so it ships with its own scheduler called WP-Cron. It borrows the name and idea, but it works very differently.
How WP-Cron Works
WP-Cron doesn't run on a clock. Instead, it's triggered by traffic:
-
A visitor loads a page: On every page load, WordPress checks whether any scheduled events are due.
-
Due events are found: WordPress stores the schedule in a single option called
cronin thewp_optionstable. If any event's timestamp is in the past, WordPress knows work needs doing. -
A background request is spawned: WordPress sends a non-blocking HTTP request to
wp-cron.phpon your own site, so the visitor's page isn't delayed. -
Events run:
wp-cron.phpfires the action hook for each due event, runs any callbacks attached to that hook, and reschedules recurring events for their next run.
This approach has one big advantage: it works everywhere, with no server configuration. But it also has two important weaknesses:
- Low-traffic sites run late: If nobody visits your site for six hours, an event scheduled for 2:00 PM won't run until the next visit. This is why scheduled posts sometimes miss their publish time.
- High-traffic sites check constantly: Every request triggers a check. WordPress uses a lock to prevent overlapping runs, but on busy sites the extra checks and loopback requests add unnecessary overhead.
Page caching can make the first problem worse. If your caching plugin or CDN serves most pages without hitting PHP, WordPress never gets the chance to check the schedule.
Default WordPress Cron Events
A fresh WordPress install already schedules several events. The exact list varies by version, but you'll typically see:
- wp_version_check, wp_update_plugins, and wp_update_themes: Check for core, plugin, and theme updates, usually twice daily.
- wp_scheduled_delete: Permanently removes posts and comments that have been in the trash for more than 30 days.
- wp_scheduled_auto_draft_delete: Cleans up old auto-drafts.
- delete_expired_transients: Removes expired transients from the database.
- wp_privacy_delete_old_export_files: Deletes old personal data export files.
- wp_site_health_scheduled_check: Runs Site Health tests in the background weekly.
- recovery_mode_clean_expired_keys: Cleans up expired recovery mode keys.
Scheduled posts use single events on the publish_future_post hook. Plugins add many more, such as backup plugins, SEO plugins, and WooCommerce.
Built-In Cron Schedules
When you schedule a recurring event, you choose a named recurrence. WordPress includes four by default:
- hourly: Every hour.
- twicedaily: Every 12 hours.
- daily: Every 24 hours.
- weekly: Every 7 days.
You can add your own intervals with the cron_schedules filter, which you'll see below.
How to View Scheduled Cron Events
Before you add your own tasks, it's useful to see what's already scheduled.
Using a Plugin
The easiest option is the free WP Crontrol plugin. After you install and activate it:
- Go to Tools > Cron Events to see every scheduled event, its hook name, its next run time, its recurrence, and the callbacks attached to it.
- Click Run now on any event to trigger it immediately, which is great for testing.
- Use Edit to change an event's schedule or Delete to remove events left behind by plugins you've uninstalled.
- Go to Settings > Cron Schedules to see available intervals and add custom ones without code.
WP Crontrol also warns you if WP-Cron appears to be broken, for example because loopback requests are failing.
Using WP-CLI
If you have SSH access, WP-CLI gives you full control from the command line:
# List all scheduled events
wp cron event list
# Show available schedules
wp cron schedule list
# Run all events that are currently due
wp cron event run --due-now
# Run a specific event immediately
wp cron event run wp_scheduled_delete
# Delete all events for a specific hook
wp cron event delete sajjad_daily_cleanup
# Test whether WP-Cron can spawn correctly
wp cron test
How to Schedule Your Own Cron Job
Now for the useful part. Scheduling a custom task takes three pieces: a function that does the work, a hook that connects the event to the function, and a call that schedules the event.
The best place for this code is a small custom plugin, so the task keeps running even if you switch themes. A child theme's functions.php also works for site-specific tasks.
Step 1: Write the Task and Attach It to a Hook
<?php
/**
* Plugin Name: Sajjad Scheduled Tasks
* Description: Custom scheduled tasks for this site.
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Delete draft posts that haven't been touched in 90 days.
*/
function sajjad_cleanup_old_drafts() {
$old_drafts = get_posts(
array(
'post_type' => 'post',
'post_status' => 'draft',
'posts_per_page' => 50,
'fields' => 'ids',
'date_query' => array(
array(
'column' => 'post_modified_gmt',
'before' => '90 days ago',
),
),
)
);
foreach ( $old_drafts as $post_id ) {
wp_trash_post( $post_id );
}
}
add_action( 'sajjad_daily_cleanup', 'sajjad_cleanup_old_drafts' );
The event itself doesn't contain code. It only stores the hook name sajjad_daily_cleanup. When the event fires, WordPress calls do_action( 'sajjad_daily_cleanup' ), and your function runs. Notice that this example moves drafts to the trash rather than deleting them permanently, which gives you a 30-day safety net.
Step 2: Schedule the Event on Activation
You only want to schedule the event once. Scheduling on every page load is a common mistake that creates duplicate events. The cleanest approach in a plugin is to schedule on activation and unschedule on deactivation:
function sajjad_activate_scheduled_tasks() {
if ( ! wp_next_scheduled( 'sajjad_daily_cleanup' ) ) {
wp_schedule_event( time(), 'daily', 'sajjad_daily_cleanup' );
}
}
register_activation_hook( __FILE__, 'sajjad_activate_scheduled_tasks' );
function sajjad_deactivate_scheduled_tasks() {
wp_clear_scheduled_hook( 'sajjad_daily_cleanup' );
}
register_deactivation_hook( __FILE__, 'sajjad_deactivate_scheduled_tasks' );
The wp_schedule_event() function takes the first run time as a Unix timestamp, the recurrence name, the hook name, and an optional array of arguments.
Scheduling From a Theme
Themes don't have activation hooks in the same way, so the common pattern is to check on init and schedule if needed. The wp_next_scheduled() check keeps this safe:
function sajjad_theme_schedule_cleanup() {
if ( ! wp_next_scheduled( 'sajjad_daily_cleanup' ) ) {
wp_schedule_event( time(), 'daily', 'sajjad_daily_cleanup' );
}
}
add_action( 'init', 'sajjad_theme_schedule_cleanup' );
Running a Task at a Specific Time of Day
If you want a daily task to run at around 3:00 AM in your site's timezone, calculate the next occurrence using WordPress's timezone settings from Settings > General:
function sajjad_next_3am_timestamp() {
$timezone = wp_timezone();
$next_run = new DateTimeImmutable( 'today 03:00', $timezone );
if ( $next_run->getTimestamp() <= time() ) {
$next_run = $next_run->modify( '+1 day' );
}
return $next_run->getTimestamp();
}
// Then schedule with:
// wp_schedule_event( sajjad_next_3am_timestamp(), 'daily', 'sajjad_daily_cleanup' );
Remember that WP-Cron only guarantees a task will run at or after this time, not exactly on it, unless you set up a real server cron job.
How to Add a Custom Cron Interval
If hourly is too slow and you need something like every 15 minutes, register a new schedule with the cron_schedules filter:
function sajjad_add_cron_intervals( $schedules ) {
$schedules['every_fifteen_minutes'] = array(
'interval' => 15 * MINUTE_IN_SECONDS,
'display' => __( 'Every 15 Minutes', 'sajjad' ),
);
return $schedules;
}
add_filter( 'cron_schedules', 'sajjad_add_cron_intervals' );
Then use it like any built-in schedule:
if ( ! wp_next_scheduled( 'sajjad_sync_inventory' ) ) {
wp_schedule_event( time(), 'every_fifteen_minutes', 'sajjad_sync_inventory' );
}
Make sure the cron_schedules filter is registered on every request, not only during activation. If WordPress can't find the interval when it tries to reschedule, the event won't recur.
How to Schedule a One-Time Event
Sometimes you want a task to run once, for example sending a follow-up email a day after someone registers. Use wp_schedule_single_event(), and pass arguments so each event is unique:
function sajjad_schedule_welcome_followup( $user_id ) {
wp_schedule_single_event(
time() + DAY_IN_SECONDS,
'sajjad_send_followup_email',
array( (int) $user_id )
);
}
add_action( 'user_register', 'sajjad_schedule_welcome_followup' );
function sajjad_send_followup_email( $user_id ) {
$user = get_userdata( $user_id );
if ( ! $user ) {
return;
}
wp_mail(
$user->user_email,
__( 'Getting started with your account', 'sajjad' ),
sprintf(
/* translators: %s: user display name */
__( 'Hi %s, here are a few tips to help you get started.', 'sajjad' ),
$user->display_name
)
);
}
add_action( 'sajjad_send_followup_email', 'sajjad_send_followup_email' );
The arguments array is passed to your callback as individual parameters. WordPress also treats the combination of hook plus arguments as the event's identity, so when you check or unschedule an event that uses arguments, pass the same array:
$timestamp = wp_next_scheduled( 'sajjad_send_followup_email', array( 42 ) );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'sajjad_send_followup_email', array( 42 ) );
}
How to Replace WP-Cron With a Real Server Cron Job
For production sites, especially low-traffic sites and sites behind heavy caching, the best practice is to stop WP-Cron from running on page loads and have the server trigger it on a fixed schedule instead. Take a backup of wp-config.php before editing it.
Step 1: Disable the Page-Load Trigger
Add this line to wp-config.php, above the line that says /* That's all, stop editing! Happy publishing. */:
define( 'DISABLE_WP_CRON', true );
This doesn't disable scheduled events. It only stops WordPress from spawning wp-cron.php on visits. Your events will still run once something else triggers the cron runner.
Step 2: Add a Server Cron Job
You now need a system cron job that runs every few minutes. Every 5 minutes is a sensible default for most sites.
Option A: WP-CLI (recommended when available)
This runs WP-Cron directly through PHP without an HTTP request, so it isn't affected by timeouts, firewalls, or caching:
*/5 * * * * cd /var/www/example.com/public_html && /usr/local/bin/wp cron event run --due-now --quiet > /dev/null 2>&1
Adjust the path to your WordPress folder and the path to wp, which you can find with which wp.
Option B: Calling wp-cron.php over HTTP
If WP-CLI isn't installed, request wp-cron.php with curl or wget:
*/5 * * * * curl -s "https://example.com/wp-cron.php?doing_wp_cron" > /dev/null 2>&1
Option C: Running wp-cron.php with PHP directly
*/5 * * * * cd /var/www/example.com/public_html && /usr/bin/php -q wp-cron.php > /dev/null 2>&1
Adding the Cron Job in cPanel
If you're on shared hosting with cPanel:
- Log in to cPanel and open Advanced > Cron Jobs.
- Under Add New Cron Job, choose Once Per Five Minutes from the Common Settings dropdown, or enter
*/5in the minute field and*in the others. - Paste one of the commands above into the Command field, adjusting the domain or path.
- Click Add New Cron Job.
Many managed WordPress hosts already run a server-side cron for you. Check your host's documentation before adding your own, so you don't end up running it twice.
Using an External Cron Service
If your host doesn't offer cron at all, a free external service like cron-job.org can request https://example.com/wp-cron.php?doing_wp_cron on a schedule. It works like Option B, just triggered from outside your server.
Troubleshooting WordPress Cron Problems
Scheduled posts show "Missed schedule": WP-Cron isn't running often enough or at all. Check whether DISABLE_WP_CRON is set without a server cron job to replace it, then set up a real cron job.
Events never run: Go to Tools > Site Health and look for failed loopback request warnings. Security plugins, basic authentication on staging sites, or firewall rules can block the request WordPress makes to wp-cron.php.
Duplicate events: Your code is scheduling without a wp_next_scheduled() check, or passes different arguments each time. Clean up with wp cron event delete <hook> and fix the scheduling code.
Events left behind by old plugins: WP Crontrol will flag events with no attached callbacks. Delete them if the plugin is gone for good.
Long-running tasks time out: Split big jobs into batches. Process a limited number of items per run, as the draft cleanup example does with posts_per_page, and let the next run continue.
Need to debug a task: Temporarily log from your callback with error_log() and enable WP_DEBUG_LOG in wp-config.php, then trigger the event with Run now or wp cron event run.
WP-Cron vs Action Scheduler
For heavy background processing, many plugins use Action Scheduler, a library created by the WooCommerce team. It stores jobs in its own database tables, processes them in batches, retries failed actions, and gives you a log at Tools > Scheduled Actions. It's still triggered by WP-Cron under the hood, so a reliable server cron job helps it too. If you're building a plugin that needs to process thousands of items, Action Scheduler is worth a look. For simple recurring tasks, WP-Cron is perfectly fine.
FAQ: WordPress Cron Jobs
WP-Cron is WordPress's built-in task scheduler. It checks for due events on page loads and runs them by requesting wp-cron.php in the background, handling tasks like scheduled posts, update checks, and cleanup.
No. A real cron job runs on a fixed schedule from the server's operating system, while WP-Cron only runs when someone visits your site. You can make it behave like a real cron by disabling the page-load trigger and adding a server cron job.
You should disable the page-load trigger with DISABLE_WP_CRON only if you replace it with a server cron job or your host already runs one. Disabling it without a replacement stops scheduled posts and maintenance tasks from running.
Every 5 minutes works well for most sites. Busy stores or sites with frequent scheduled tasks may use every minute, while simple blogs can use every 10 to 15 minutes.
Scheduled posts rely on WP-Cron. Low traffic, aggressive page caching, failed loopback requests, or DISABLE_WP_CRON without a replacement can all stop the publish event from firing on time.
Install the WP Crontrol plugin and go to Tools > Cron Events, or run wp cron event list with WP-CLI.
Put it in a small custom plugin so it keeps running if you change themes. That also lets you use activation and deactivation hooks to schedule and clean up events properly.
Conclusion
WordPress cron jobs let you automate almost anything, from cleaning up old drafts to syncing data with external services. The key ideas are simple: attach your code to a custom hook, schedule an event for that hook once with wp_schedule_event() or wp_schedule_single_event(), and clean up after yourself with wp_clear_scheduled_hook() when your plugin is deactivated.
The built-in WP-Cron is fine for many sites, but it depends on traffic. For anything that needs to run on time, disable the page-load trigger with DISABLE_WP_CRON and add a real server cron job, ideally using WP-CLI. Pair that with WP Crontrol for visibility, and you'll always know what's scheduled on your site and whether it's actually running.


