WP Tuts Hub

Practical WordPress tutorials for developers
Advertisement

Browse

  • Home
  • Categories
    • WordPress & AI
    • WordPress Security
    • WordPress Troubleshooting
    • WordPress Tips and Tricks
    • SSH (Secure Shell)
    • WordPress Insights
    • WordPress Common Facts
  • Series Tutorials
    • Advanced WordPress Topic
  • My Plugins
  • Contact Me
  • About WP Tuts Hub
Home/Series Tutorials/WP-Cron Part 1: Schedule Recurring Events in WordPress
Series Tutorials
By Md. Zubaer Ahammed Updated November 12, 2026

WP-Cron Part 1: Schedule Recurring Events in WordPress

  • Schedule Events
  • Using Cron Job
  • WordPress Cron Job
  • WP-Cron

WordPress has its own scheduler, WP-Cron, for jobs that should run on a timetable: clearing expired data, sending digests, syncing with an API. In this first part of the series you will build a small plugin that schedules a recurring job properly, adds a custom interval, cleans up after itself, and runs reliably on a real server cron. Part 2 covers single events, arguments, and how to inspect and debug scheduled jobs.

Note Updated in November 2026. The plugin below was checked on WordPress 7.1.3 with PHP 8.2.

How WP-Cron differs from a real cron

A system cron runs at exact times whether or not anyone visits your site. WP-Cron does not. On each page load, WordPress checks whether any scheduled job is due and, if so, runs it in a background request. So:

  • A job scheduled for 03:00 runs on the first visit after 03:00. On a quiet site that might be hours later.
  • On a busy site the check happens on many requests, which adds a little overhead.

For low-traffic sites and anything time-sensitive, you will connect WP-Cron to a real system cron at the end of this article.

The example: a token cleanup job every 15 minutes

A plugin that deletes expired rows from a custom table every 15 minutes. The pattern is the same for any recurring job.

Step 1 Create the plugin and the job

Create wp-content/plugins/wth-token-cleanup/wth-token-cleanup.php:

<?php
/**
 * Plugin Name: WTH Token Cleanup
 * Description: Deletes expired tokens every 15 minutes with WP-Cron.
 * Version: 1.0.0
 */

defined( 'ABSPATH' ) || exit;

const WTH_CLEANUP_HOOK = 'wth_cleanup_expired_tokens';

// 1. Add a custom interval: WordPress only ships hourly, twicedaily, daily and weekly.
add_filter( 'cron_schedules', function ( array $schedules ): array {
    $schedules['wth_every_fifteen_minutes'] = array(
        'interval' => 15 * MINUTE_IN_SECONDS,
        'display'  => __( 'Every 15 minutes' ),
    );
    return $schedules;
} );

// 2. The job itself: runs whenever the hook fires.
add_action( WTH_CLEANUP_HOOK, function (): void {
    global $wpdb;
    $table = $wpdb->prefix . 'wth_tokens';

    $deleted = $wpdb->query(
        $wpdb->prepare( "DELETE FROM {$table} WHERE expires_at < %s", current_time( 'mysql', true ) )
    );

    update_option( 'wth_last_cleanup', array( 'time' => time(), 'deleted' => (int) $deleted ), false );
} );

A scheduled job is just an action hook. WP-Cron’s only role is to call do_action( 'wth_cleanup_expired_tokens' ) at the right time.

Step 2 Schedule it once, on activation

Add an activation hook to the same file:

register_activation_hook( __FILE__, function (): void {
    if ( ! wp_next_scheduled( WTH_CLEANUP_HOOK ) ) {
        wp_schedule_event( time(), 'wth_every_fifteen_minutes', WTH_CLEANUP_HOOK );
    }
} );

wp_schedule_event( $timestamp, $recurrence, $hook, $args ) takes:

  • $timestamp: when the first run is due, as a Unix timestamp. time() means as soon as possible.
  • $recurrence: the name of an interval, such as hourly, twicedaily, daily, weekly or your own.
  • $hook: the action to run.
  • $args: optional arguments passed to the hook. Part 2 covers these.

Important Always check wp_next_scheduled() first. Without the check, every call adds another copy of the event. In our test, two calls with different start times left two copies of the same job in the schedule, so it ran twice as often. Many older tutorials schedule on init, which runs on every page load. Scheduling on activation, with the guard, is both cheaper and safer.

Two wp_schedule_event() calls without the guard, then the cron list (test site: WordPress 7.1.3, PHP 8.2):

$ wp eval 'wp_schedule_event( time(), "hourly", "wth_demo_job" ); wp_schedule_event( time() + 60, "hourly", "wth_demo_job" );'

$ wp cron event list --fields=hook,next_run_relative,recurrence | grep -E '^hook|wth_demo_job'
hook	next_run_relative	recurrence
wth_demo_job	now	1 hour
wth_demo_job	59 seconds	1 hour

Step 3 Unschedule on deactivation

If you deactivate the plugin without removing the event, WordPress keeps trying to run a hook that no longer exists. Clear it:

register_deactivation_hook( __FILE__, function (): void {
    wp_clear_scheduled_hook( WTH_CLEANUP_HOOK );
} );

wp_clear_scheduled_hook() removes every scheduled copy of the hook and returns how many it removed. To remove a single occurrence instead, use wp_unschedule_event( wp_next_scheduled( WTH_CLEANUP_HOOK ), WTH_CLEANUP_HOOK ).

Step 4 Check that it is scheduled

Activate the plugin, then check from code or WP-CLI:

$next = wp_next_scheduled( WTH_CLEANUP_HOOK );          // Unix timestamp, or false
$event = wp_get_scheduled_event( WTH_CLEANUP_HOOK );   // ->schedule, ->interval, ->timestamp
wp cron event list --fields=hook,next_run_relative,recurrence
wp cron event run wth_cleanup_expired_tokens

In our test the event reported the schedule wth_every_fifteen_minutes with an interval of 900 seconds, and running the hook called the job once.

The plugin activated with five tokens, three of them expired (test site: WordPress 7.1.3, PHP 8.2):

$ wp eval 'var_dump( wp_next_scheduled( "wth_cleanup_expired_tokens" ) ); print_r( wp_get_scheduled_event( "wth_cleanup_expired_tokens" ) );'
int(1794514649)
stdClass Object
(
    [hook] => wth_cleanup_expired_tokens
    [timestamp] => 1794514649
    [schedule] => wth_every_fifteen_minutes
    [args] => Array
        (
        )

    [interval] => 900
)

$ wp cron event list --fields=hook,next_run_relative,recurrence | grep -E '^hook|wth_'
hook	next_run_relative	recurrence
wth_cleanup_expired_tokens	now	15 minutes

$ wp cron event run wth_cleanup_expired_tokens
Executed the cron event 'wth_cleanup_expired_tokens' in 0.005s.
Success: Executed a total of 1 cron event.

$ wp option get wth_last_cleanup --format=json
{"time":1794514650,"deleted":3}

Tip If wp_schedule_event() fails, pass true as the fifth argument to get a WP_Error instead of false. Scheduling with an interval that is not registered returns the error code invalid_schedule, which usually means your cron_schedules filter did not run.

Scheduling with an interval that is not registered, with the fifth argument set to true (test site: WordPress 7.1.3, PHP 8.2):

$ wp eval '$r = wp_schedule_event( time(), "every_ten_seconds", "wth_demo_job2", array(), true ); echo get_class( $r ), ": ", $r->get_error_code(), " - ", $r->get_error_message(), "\n";'
WP_Error: invalid_schedule - Event schedule does not exist.

Step 5 Run WP-Cron from a real cron

To run jobs on time regardless of traffic, stop WordPress triggering cron on page loads and call it from the server instead. Add this to wp-config.php:

define( 'DISABLE_WP_CRON', true );

Then add a system cron job (crontab -e) that runs every five minutes. With WP-CLI:

*/5 * * * * cd /var/www/html && wp cron event run --due-now --quiet

Without WP-CLI, request wp-cron.php directly:

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

Many hosts offer a cron setting in their control panel if you cannot edit crontab.

Warning Once DISABLE_WP_CRON is set, nothing runs scheduled jobs until the system cron is in place, including WordPress’s own update checks and scheduled posts. Add the cron job first, then the constant.

Five mistakes that stop WP-Cron jobs running properly

  • Scheduling on every page load without wp_next_scheduled(), which piles up duplicate events.
  • Using a custom interval name before the cron_schedules filter is added.
  • Forgetting to clear the event on deactivation.
  • Expecting exact timing on a low-traffic site without a real cron.
  • Doing slow work, such as large imports, in a single cron run. Split it into batches, or use a queue such as Action Scheduler.

Where Part 2 picks up

You now have a recurring job that schedules itself once, cleans up after itself, and can run on time from a system cron. In Part 2 you will schedule one-off events, pass arguments, and build a small admin page to see every scheduled job.

Official reference Cron in the Plugin Handbook · wp_schedule_event() · Hooking WP-Cron into the system task scheduler · wp cron

Share this:

  • Share on X (Opens in new window) X
  • Share on Facebook (Opens in new window) Facebook
  • Share on LinkedIn (Opens in new window) LinkedIn
  • Share on WhatsApp (Opens in new window) WhatsApp
  • Share on Reddit (Opens in new window) Reddit
  • Email a link to a friend (Opens in new window) Email

Related

Advertisement
Previous Article How to Secure WordPress Site From Hacking, DDoS and Brute Force Attack
Next Article WP-Cron Part 2: Single Events, Arguments and Debugging

About the author

Md. Zubaer Ahammed
Md. Zubaer Ahammed

Md. Zubaer Ahammed is a senior full-stack developer building learning platforms and modern web systems with WordPress, Laravel, Python/Django, Vue, and React. More at https://zubaer.com/.

I also build WordPress plugins, including ZubFeat AI Featured Image and ZubQuant. See my plugins and services →

Related Posts

  • Harden WordPress Application Passwords: Per-Role Availability, Revocation on Role Change and Auth Logging Harden WordPress Application Passwords: Per-Role Availability, Revocation on Role Change and Auth Logging
  • Build a Streaming WordPress AI Chat Assistant: SSE Endpoint, Conversation Table and Per-User Rate Limits Build a Streaming WordPress AI Chat Assistant: SSE Endpoint, Conversation Table and Per-User Rate Limits

2 Comments

  1. Pingback: Using Cron Job in WordPress – Schedule Events With WP-Cron: Part 2 - WP Tuts Hub
  2. Pingback: (Türkçe) Cronjob Nedir? Nasıl Kullanılır? - Ceyhun Enki Aksan

Leave a Reply

Cancel reply

WP Tuts Hub

WordPress tutorials, themes, and plugins — written for builders.

  • Portfolio
  • My plugins
  • Contact
  • About
  • Privacy
  • Terms

© 2026 WP Tuts Hub · Md. Zubaer Ahammed