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.