Schedule recurring tasks in WordPress with WP-Cron

Many WordPress features depend on tasks that run on a schedule rather than in response to a user request: sending scheduled emails, cleaning up expired session tokens, publishing posts scheduled for a future date, checking for plugin updates, and regenerating sitemaps after new content is published. WordPress implements all of these through a built-in pseudo-cron system called WP-Cron, which simulates time-based task scheduling without requiring server-level cron job access. The mechanism works by checking a list of scheduled events on every front-end page load and executing any tasks whose scheduled time has passed. This means WP-Cron only fires when someone visits the site — on a low-traffic site with an hourly task scheduled, that task might not run for several hours after its due time if no page load triggers the check. On high-traffic sites the opposite problem can occur: multiple concurrent page loads may all trigger the same scheduled task simultaneously before the lock mechanism prevents duplication. For production sites with reliable server access, replacing WP-Cron with a real system cron job that calls wp-cron.php at precise intervals solves both problems and is the recommended approach. For development environments and sites without server-level cron access, the built-in system works well enough. The WP-Cron API has three main operations: scheduling an event with wp_schedule_event(), defining the task as an action with add_action(), and cleaning up by unscheduling on plugin deactivation with wp_clear_scheduled_hook(). The built-in schedule intervals are hourly, twicedaily, and daily — custom intervals require registering them through the cron_schedules filter. The cleanup step on plugin deactivation is critical: a plugin that schedules tasks but never unschedules them leaves orphaned cron entries in the database that continue running forever after the plugin is removed. This is a common source of unexplained periodic PHP errors in WordPress sites where a plugin has been removed but its scheduled hooks were never cleaned up. The example below schedules a weekly database cleanup task, adding to the optimization techniques covered in our post on optimizing WordPress database tables.

Problem: You need to run a PHP task automatically on a recurring schedule without server-level cron job access.

Solution: Add the following code to your plugin or functions.php file:

<?php
// 1. Register a custom interval (every 5 minutes)
add_filter( 'cron_schedules', 'ha_add_cron_intervals' );

function ha_add_cron_intervals( $schedules ) {
    $schedules['every_five_minutes'] = array(
        'interval' => 300,
        'display'  => 'Every 5 Minutes',
    );
    $schedules['weekly'] = array(
        'interval' => WEEK_IN_SECONDS,
        'display'  => 'Once Weekly',
    );
    return $schedules;
}

// 2. Schedule the event on plugin/theme activation (runs once)
add_action( 'wp', 'ha_schedule_weekly_cleanup' );

function ha_schedule_weekly_cleanup() {
    if ( ! wp_next_scheduled( 'ha_weekly_cleanup_event' ) ) {
        wp_schedule_event( time(), 'weekly', 'ha_weekly_cleanup_event' );
    }
}

// 3. Define the task that runs on schedule
add_action( 'ha_weekly_cleanup_event', 'ha_run_weekly_cleanup' );

function ha_run_weekly_cleanup() {
    global $wpdb;

    // Delete expired transients
    $wpdb->query(
        "DELETE FROM {$wpdb->options}
         WHERE option_name LIKE '%_transient_timeout_%'
         AND option_value < UNIX_TIMESTAMP()"
    );

    // Log the run time (optional)
    update_option( 'ha_last_cleanup_run', current_time( 'mysql' ) );
}

// 4. Unschedule on plugin deactivation to prevent orphaned hooks
register_deactivation_hook( __FILE__, 'ha_clear_scheduled_cleanup' );

function ha_clear_scheduled_cleanup() {
    $timestamp = wp_next_scheduled( 'ha_weekly_cleanup_event' );
    if ( $timestamp ) {
        wp_unschedule_event( $timestamp, 'ha_weekly_cleanup_event' );
    }
}

NOTE: register_deactivation_hook() only works inside a plugin file — if you place this code in functions.php, replace it with a manual unscheduling call on a separate admin action. To verify that your scheduled event is registered correctly, install the WP Crontrol plugin temporarily — it lists all scheduled hooks, their intervals, and next run times in a readable admin table. On production servers, disable the built-in WP-Cron by adding define( 'DISABLE_WP_CRON', true ); to wp-config.php and create a real system cron job: */5 * * * * curl -s https://yourdomain.com/wp-cron.php?doing_wp_cron.