Complete WordPress Cron Guide: Hooks, Methods & Best Practices
Table of Contents
Master the WordPress WP-Cron system with production-ready examples, security best practices, and performance optimization techniques. This comprehensive guide covers everything from basic scheduling to advanced custom intervals and debugging.
What is WP-Cron and How It Works
WordPress WP-Cron is a time-based task scheduling system that executes scheduled events without requiring server-level cron access. Unlike traditional Unix cron that runs at specific intervals regardless of traffic, WP-Cron is triggered by page loads, making it both a strength and potential limitation.
How WP-Cron Execution Works
When a user visits your WordPress site, the system checks if any scheduled tasks are due. If tasks are ready to execute, WordPress spawns a non-blocking HTTP request to wp-cron.php that processes the events in the background. This approach has several implications:
- Traffic Dependency: Low-traffic sites may experience delayed execution since cron relies on page visits
- Non-Blocking Execution: The visitor doesn’t wait for cron tasks to complete
- No Server Access Required: Works on any hosting environment without shell access
- Potential Overlap: Multiple simultaneous visits can trigger duplicate executions
WP-Cron vs System Cron
| Feature | WP-Cron | System Cron |
|---|---|---|
| Execution Trigger | Page loads | Time-based intervals |
| Server Access Required | No | Yes (SSH/cPanel) |
| Precision | Approximate (traffic-dependent) | Exact timing |
| Low-Traffic Sites | Can be unreliable | Reliable |
| Resource Usage | Distributed across requests | Dedicated execution |
Basic Event Scheduling
WordPress provides three primary functions for scheduling events, each serving different use cases. Understanding when to use each method is crucial for efficient task management.
Single Event Scheduling
Use wp_schedule_single_event() when you need a task to run exactly once at a specific time. Common use cases include sending delayed emails, processing one-time imports, or scheduling content publication.
/**
* Schedule a single event to run in 2 hours
*
* @return void
*/
function schedule_delayed_notification() {
// Ensure we don't duplicate the event
$timestamp = time() + ( 2 * HOUR_IN_SECONDS );
$hook = 'send_delayed_email_notification';
$args = array( 'user_id' => 42 );
// Check if already scheduled
if ( ! wp_next_scheduled( $hook, $args ) ) {
wp_schedule_single_event( $timestamp, $hook, $args );
}
}
add_action( 'init', 'schedule_delayed_notification' );
/**
* Handle the scheduled event
*
* @param int $user_id User ID to send notification to.
* @return void
*/
function send_delayed_email_notification( $user_id ) {
$user = get_userdata( $user_id );
if ( ! $user ) {
return;
}
$to = $user->user_email;
$subject = 'Your Scheduled Notification';
$message = 'This email was sent via WP-Cron 2 hours after scheduling.';
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
wp_mail( $to, $subject, $message, $headers );
}
add_action( 'send_delayed_email_notification', 'send_delayed_email_notification', 10, 1 );Recurring Event Scheduling
For tasks that need to repeat at regular intervals, use wp_schedule_event(). This is ideal for cleanup tasks, data synchronization, report generation, and automated maintenance.
/**
* Schedule a recurring daily cleanup task
*
* @return void
*/
function schedule_daily_cleanup() {
$timestamp = strtotime( 'tomorrow 2:00am' ); // Run at 2 AM daily
$recurrence = 'daily';
$hook = 'perform_daily_cleanup';
if ( ! wp_next_scheduled( $hook ) ) {
wp_schedule_event( $timestamp, $recurrence, $hook );
}
}
add_action( 'wp', 'schedule_daily_cleanup' );
/**
* Perform daily cleanup operations
*
* @return void
*/
function perform_daily_cleanup() {
global $wpdb;
// Delete transients older than 30 days
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE %s
AND option_name NOT LIKE %s
AND option_value < %d", $wpdb->esc_like( '_transient_timeout_' ) . '%',
$wpdb->esc_like( '_transient_timeout_feed_' ) . '%',
time() - ( 30 * DAY_IN_SECONDS )
)
);
// Clear old post revisions (keep last 5)
$wpdb->query(
"DELETE p1 FROM {$wpdb->posts} p1
INNER JOIN {$wpdb->posts} p2
WHERE p1.post_parent = p2.post_parent
AND p1.post_type = 'revision'
AND p1.ID < p2.ID AND ( SELECT COUNT(*) FROM {$wpdb->posts} p3
WHERE p3.post_parent = p1.post_parent
AND p3.post_type = 'revision'
AND p3.ID > p1.ID
) >= 5"
);
do_action( 'after_daily_cleanup' );
}
add_action( 'perform_daily_cleanup', 'perform_daily_cleanup' );Core Scheduling Methods
wp_schedule_single_event()
Schedules a hook to run once at a specific timestamp. This event will only execute a single time and then remove itself from the schedule.
Parameters:
$timestamp(int) – Unix timestamp when the event should run$hook(string) – Action hook to execute$args(array) – Optional arguments to pass to the hook callback$wp_error(bool) – Return WP_Error on failure (WordPress 5.1+)
/**
* Schedule post auto-save 5 minutes from now
*
* @param int $post_id Post ID to auto-save.
* @return bool|WP_Error True on success, WP_Error on failure.
*/
function schedule_post_auto_save( $post_id ) {
$timestamp = time() + ( 5 * MINUTE_IN_SECONDS );
$hook = 'auto_save_post_draft';
$args = array( 'post_id' => absint( $post_id ) );
return wp_schedule_single_event( $timestamp, $hook, $args, true );
}
/**
* Auto-save post draft
*
* @param int $post_id Post ID.
* @return void
*/
function auto_save_post_draft( $post_id ) {
// Verify post exists and is not published
$post = get_post( $post_id );
if ( ! $post || 'publish' === $post->post_status ) {
return;
}
// Save current content
$post_data = array(
'ID' => $post_id,
'post_content' => $post->post_content,
'post_title' => $post->post_title,
);
wp_update_post( $post_data );
update_post_meta( $post_id, '_last_auto_save', current_time( 'mysql' ) );
}
add_action( 'auto_save_post_draft', 'auto_save_post_draft', 10, 1 );wp_schedule_event()
Schedules a recurring event using predefined or custom intervals. The event will continue to execute at the specified interval until manually unscheduled.
Parameters:
$timestamp(int) – Unix timestamp for first execution$recurrence(string) – How often the event should recur (hourly, twicedaily, daily, or custom)$hook(string) – Action hook to execute$args(array) – Optional arguments passed to hook callback$wp_error(bool) – Return WP_Error on failure (WordPress 5.1+)
/**
* Schedule hourly API data sync
*
* @return void
*/
function schedule_api_sync() {
$timestamp = time();
$recurrence = 'hourly';
$hook = 'sync_external_api_data';
if ( ! wp_next_scheduled( $hook ) ) {
wp_schedule_event( $timestamp, $recurrence, $hook );
}
}
add_action( 'init', 'schedule_api_sync' );
/**
* Sync data from external API
*
* @return void
*/
function sync_external_api_data() {
$api_url = 'https://api.example.com/data';
$response = wp_remote_get(
$api_url,
array(
'timeout' => 15,
'headers' => array(
'Authorization' => 'Bearer ' . get_option( 'api_token' ),
),
)
);
if ( is_wp_error( $response ) ) {
error_log( 'API Sync Failed: ' . $response->get_error_message() );
return;
}
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );
if ( json_last_error() !== JSON_ERROR_NONE ) {
error_log( 'API Sync JSON Error: ' . json_last_error_msg() );
return;
}
update_option( 'synced_api_data', $data );
update_option( 'last_api_sync', current_time( 'timestamp' ) );
}
add_action( 'sync_external_api_data', 'sync_external_api_data' );wp_next_scheduled()
Returns the timestamp of the next scheduled occurrence of an event, or false if not scheduled. Essential for preventing duplicate scheduling.
<?php
/**
* Check if backup is already scheduled
*
* @return int|false Next scheduled timestamp or false.
*/
function get_next_backup_time() {
$hook = 'perform_database_backup';
$args = array( 'backup_type' => 'full' );
return wp_next_scheduled( $hook, $args );
}
/**
* Display next backup time in admin
*
* @return void
*/
function display_next_backup_notice() {
$next_backup = get_next_backup_time();
if ( $next_backup ) {
$time_diff = human_time_diff( time(), $next_backup );
echo '<div class="notice notice-info">';
echo 'Next backup scheduled in ' . esc_html( $time_diff ) . '
';
echo '</div>';
}
}
add_action( 'admin_notices', 'display_next_backup_notice' );Custom Cron Intervals
WordPress includes three default intervals: hourly (3600 seconds), twicedaily (43200 seconds), and daily (86400 seconds). For custom timing requirements, you can add your own intervals using the cron_schedules filter.
Adding Custom Intervals
Custom intervals are useful for specific business requirements like every 5 minutes, weekly, or monthly schedules. Always use the cron_schedules filter hook to add intervals.
/**
* Add custom cron intervals
*
* @param array $schedules Existing cron schedules.
* @return array Modified schedules array.
*/
function add_custom_cron_intervals( $schedules ) {
// Every 5 minutes
$schedules['every_five_minutes'] = array(
'interval' => 5 * MINUTE_IN_SECONDS,
'display' => esc_html__( 'Every 5 Minutes', 'textdomain' ),
);
// Every 15 minutes
$schedules['every_fifteen_minutes'] = array(
'interval' => 15 * MINUTE_IN_SECONDS,
'display' => esc_html__( 'Every 15 Minutes', 'textdomain' ),
);
// Every 30 minutes
$schedules['every_thirty_minutes'] = array(
'interval' => 30 * MINUTE_IN_SECONDS,
'display' => esc_html__( 'Every 30 Minutes', 'textdomain' ),
);
// Weekly
$schedules['weekly'] = array(
'interval' => 7 * DAY_IN_SECONDS,
'display' => esc_html__( 'Once Weekly', 'textdomain' ),
);
// Monthly (approximate - 30 days)
$schedules['monthly'] = array(
'interval' => 30 * DAY_IN_SECONDS,
'display' => esc_html__( 'Once Monthly', 'textdomain' ),
);
return $schedules;
}
add_filter( 'cron_schedules', 'add_custom_cron_intervals' );Using Custom Intervals
Once registered, custom intervals can be used exactly like built-in intervals with wp_schedule_event().
/**
* Schedule frequent cache warming every 5 minutes
*
* @return void
*/
function schedule_cache_warming() {
$timestamp = time();
$recurrence = 'every_five_minutes';
$hook = 'warm_site_cache';
if ( ! wp_next_scheduled( $hook ) ) {
wp_schedule_event( $timestamp, $recurrence, $hook );
}
}
add_action( 'init', 'schedule_cache_warming' );
/**
* Warm critical pages cache
*
* @return void
*/
function warm_site_cache() {
$critical_urls = array(
home_url( '/' ),
home_url( '/about/' ),
home_url( '/contact/' ),
home_url( '/products/' ),
);
foreach ( $critical_urls as $url ) {
wp_remote_get(
$url,
array(
'timeout' => 10,
'blocking' => false,
'sslverify' => false,
)
);
}
}
add_action( 'warm_site_cache', 'warm_site_cache' );Weekly Report Generation Example
/**
* Schedule weekly analytics report
*
* @return void
*/
function schedule_weekly_report() {
// Run every Monday at 9 AM
$next_monday = strtotime( 'next Monday 9:00am' );
$recurrence = 'weekly';
$hook = 'generate_weekly_analytics_report';
if ( ! wp_next_scheduled( $hook ) ) {
wp_schedule_event( $next_monday, $recurrence, $hook );
}
}
add_action( 'wp', 'schedule_weekly_report' );
/**
* Generate and email weekly analytics report
*
* @return void
*/
function generate_weekly_analytics_report() {
global $wpdb;
$last_week = strtotime( '-7 days' );
// Get post stats
$post_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->posts}
WHERE post_status = 'publish'
AND post_type = 'post'
AND post_date >= %s",
gmdate( 'Y-m-d H:i:s', $last_week )
)
);
// Get comment stats
$comment_count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(*) FROM {$wpdb->comments}
WHERE comment_approved = '1'
AND comment_date >= %s",
gmdate( 'Y-m-d H:i:s', $last_week )
)
);
// Build email
$admin_email = get_option( 'admin_email' );
$subject = sprintf( 'Weekly Analytics Report - %s', gmdate( 'F j, Y' ) );
$message = sprintf(
"Weekly Report Summary:\n\n" .
"New Posts: %d\n" .
"New Comments: %d\n" .
"Report Period: %s to %s\n",
$post_count,
$comment_count,
gmdate( 'M j', $last_week ),
gmdate( 'M j' )
);
wp_mail( $admin_email, $subject, $message );
}
add_action( 'generate_weekly_analytics_report', 'generate_weekly_analytics_report' );Cron Hooks and Actions
WordPress provides several hooks that allow you to interact with the cron system at various stages of execution. Understanding these hooks enables advanced functionality like logging, error handling, and execution control.
Core Cron Hooks
cron_schedules Filter
Already covered above, this filter allows you to add custom intervals to the available schedules.
schedule_event Action Hook
Fires immediately after an event is scheduled. Useful for logging or triggering additional actions when events are created.
/**
* Log when events are scheduled
*
* @param object $event Scheduled event object.
* @return void
*/
function log_cron_event_scheduled( $event ) {
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
return;
}
error_log(
sprintf(
'Cron Event Scheduled: %s | Next Run: %s | Interval: %s',
$event->hook,
gmdate( 'Y-m-d H:i:s', $event->timestamp ),
isset( $event->schedule ) ? $event->schedule : 'single'
)
);
}
add_action( 'schedule_event', 'log_cron_event_scheduled', 10, 1 );unschedule_event Action Hook
Fires when an event is unscheduled, allowing you to perform cleanup or logging operations.
/**
* Log when events are unscheduled
*
* @param int $timestamp Timestamp of the event.
* @param string $hook Action hook of the event.
* @param array $args Arguments passed to the hook.
* @return void
*/
function log_cron_event_unscheduled( $timestamp, $hook, $args ) {
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) {
return;
}
error_log(
sprintf(
'Cron Event Unscheduled: %s | Was scheduled for: %s',
$hook,
gmdate( 'Y-m-d H:i:s', $timestamp )
)
);
}
add_action( 'unschedule_event', 'log_cron_event_unscheduled', 10, 3 );wp_cron Action Hook
Fires before WP-Cron executes scheduled events. Perfect for logging, monitoring, or initialization tasks that should run before any cron job.
/**
* Log WP-Cron execution start
*
* @return void
*/
function log_wp_cron_execution() {
$crons = _get_cron_array();
$count = 0;
if ( is_array( $crons ) ) {
foreach ( $crons as $timestamp => $cronhooks ) {
$count += count( $cronhooks );
}
}
error_log(
sprintf(
'WP-Cron Started | Total scheduled events: %d | Timestamp: %s',
$count,
gmdate( 'Y-m-d H:i:s' )
)
);
}
add_action( 'wp_cron', 'log_wp_cron_execution' );pre_schedule_event Filter
Filters an event before it is scheduled. Return false to prevent the event from being scheduled.
/**
* Prevent scheduling events during maintenance mode
*
* @param null|bool|WP_Error $pre Pre-schedule value (null to continue).
* @param object $event Event object.
* @param bool $wp_error Whether to return WP_Error.
* @return null|bool|WP_Error
*/
function prevent_cron_during_maintenance( $pre, $event, $wp_error ) {
// Check if maintenance mode is active
if ( file_exists( ABSPATH . '.maintenance' ) ) {
if ( $wp_error ) {
return new WP_Error(
'maintenance_mode',
__( 'Cannot schedule events during maintenance mode.', 'textdomain' )
);
}
return false;
}
return $pre;
}
add_filter( 'pre_schedule_event', 'prevent_cron_during_maintenance', 10, 3 );Advanced Real-World Examples
Complete Backup System with Static Class
A production-ready backup system that handles scheduling, execution, cleanup, and error handling with proper WPCS compliance.
/**
* Database Backup Manager
*
* Handles automated database backups with cleanup and error handling.
*
* @package YourPlugin
*/
class Database_Backup_Manager {
/**
* Backup directory path
*
* @var string
*/
private static $backup_dir = '';
/**
* Initialize backup system
*
* @return void
*/
public static function init() {
self::$backup_dir = WP_CONTENT_DIR . '/backups/';
// Create backup directory if it doesn't exist
if ( ! file_exists( self::$backup_dir ) ) {
wp_mkdir_p( self::$backup_dir );
}
// Schedule daily backups at 3 AM
add_action( 'init', array( __CLASS__, 'schedule_backups' ) );
add_action( 'perform_daily_database_backup', array( __CLASS__, 'perform_backup' ) );
add_action( 'cleanup_old_backups', array( __CLASS__, 'cleanup_old_backups' ) );
}
/**
* Schedule backup events
*
* @return void
*/
public static function schedule_backups() {
// Schedule daily backup
if ( ! wp_next_scheduled( 'perform_daily_database_backup' ) ) {
wp_schedule_event(
strtotime( 'tomorrow 3:00am' ),
'daily',
'perform_daily_database_backup'
);
}
// Schedule weekly cleanup
if ( ! wp_next_scheduled( 'cleanup_old_backups' ) ) {
wp_schedule_event(
strtotime( 'next Sunday 4:00am' ),
'weekly',
'cleanup_old_backups'
);
}
}
/**
* Perform database backup
*
* @return bool True on success, false on failure.
*/
public static function perform_backup() {
global $wpdb;
$filename = sprintf(
'db-backup-%s-%s.sql',
sanitize_title( get_bloginfo( 'name' ) ),
gmdate( 'Y-m-d-H-i-s' )
);
$filepath = self::$backup_dir . $filename;
// Get all tables
$tables = $wpdb->get_results( 'SHOW TABLES', ARRAY_N );
if ( empty( $tables ) ) {
error_log( 'Database backup failed: No tables found' );
return false;
}
$sql_dump = '';
// Loop through tables
foreach ( $tables as $table ) {
$table_name = $table[0];
// Get CREATE TABLE statement
$create_table = $wpdb->get_row( "SHOW CREATE TABLE `{$table_name}`", ARRAY_N );
$sql_dump .= "\n\n" . $create_table[1] . ";\n\n";
// Get table data
$rows = $wpdb->get_results( "SELECT * FROM `{$table_name}`", ARRAY_A );
if ( ! empty( $rows ) ) {
foreach ( $rows as $row ) {
$values = array_map(
function( $value ) use ( $wpdb ) {
return is_null( $value ) ? 'NULL' : "'" . $wpdb->_real_escape( $value ) . "'";
},
array_values( $row )
);
$sql_dump .= sprintf(
"INSERT INTO `%s` VALUES (%s);\n",
$table_name,
implode( ', ', $values )
);
}
}
}
// Write to file
$result = file_put_contents( $filepath, $sql_dump );
if ( false === $result ) {
error_log( 'Database backup failed: Could not write to file' );
return false;
}
// Compress backup
if ( function_exists( 'gzencode' ) ) {
$compressed = gzencode( $sql_dump, 9 );
file_put_contents( $filepath . '.gz', $compressed );
unlink( $filepath ); // Remove uncompressed version
}
// Log success
update_option( 'last_backup_time', current_time( 'timestamp' ) );
update_option( 'last_backup_file', $filename );
return true;
}
/**
* Cleanup old backups
*
* @param int $days_to_keep Number of days to retain backups.
* @return void
*/
public static function cleanup_old_backups( $days_to_keep = 30 ) {
$files = glob( self::$backup_dir . 'db-backup-*.sql*' );
if ( empty( $files ) ) {
return;
}
$cutoff_time = time() - ( $days_to_keep * DAY_IN_SECONDS );
$deleted = 0;
foreach ( $files as $file ) {
if ( filemtime( $file ) < $cutoff_time ) { unlink( $file ); $deleted++; } } if ( $deleted > 0 ) {
error_log( sprintf( 'Cleaned up %d old backup files', $deleted ) );
}
}
/**
* Get backup status
*
* @return array Backup status information.
*/
public static function get_backup_status() {
$last_backup = get_option( 'last_backup_time', 0 );
$next_backup = wp_next_scheduled( 'perform_daily_database_backup' );
return array(
'last_backup' => $last_backup ? gmdate( 'Y-m-d H:i:s', $last_backup ) : 'Never',
'next_backup' => $next_backup ? gmdate( 'Y-m-d H:i:s', $next_backup ) : 'Not scheduled',
'last_backup_file' => get_option( 'last_backup_file', 'None' ),
);
}
}
// Initialize the backup system
Database_Backup_Manager::init();Email Queue System with Rate Limiting
A sophisticated email queuing system that prevents overwhelming SMTP servers by rate-limiting email sends.
/**
* Email Queue Manager
*
* Manages queued emails with rate limiting to prevent SMTP throttling.
*
* @package YourPlugin
*/
class Email_Queue_Manager {
/**
* Queue table name
*
* @var string
*/
private static $table_name = '';
/**
* Maximum emails per batch
*
* @var int
*/
private static $batch_size = 50;
/**
* Initialize email queue system
*
* @return void
*/
public static function init() {
global $wpdb;
self::$table_name = $wpdb->prefix . 'email_queue';
// Create database table
add_action( 'plugins_loaded', array( __CLASS__, 'create_table' ) );
// Schedule queue processing every 5 minutes
add_action( 'init', array( __CLASS__, 'schedule_processing' ) );
add_action( 'process_email_queue', array( __CLASS__, 'process_queue' ) );
}
/**
* Create queue table
*
* @return void
*/
public static function create_table() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS " . self::$table_name . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
recipient varchar(255) NOT NULL,
subject text NOT NULL,
message longtext NOT NULL,
headers longtext,
attachments longtext,
priority tinyint(1) DEFAULT 5,
status varchar(20) DEFAULT 'pending',
attempts int(11) DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
sent_at datetime DEFAULT NULL,
PRIMARY KEY (id),
KEY status (status),
KEY priority (priority),
KEY created_at (created_at)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
/**
* Schedule queue processing
*
* @return void
*/
public static function schedule_processing() {
if ( ! wp_next_scheduled( 'process_email_queue' ) ) {
wp_schedule_event( time(), 'every_five_minutes', 'process_email_queue' );
}
}
/**
* Add email to queue
*
* @param string $to Recipient email address.
* @param string $subject Email subject.
* @param string $message Email message.
* @param array $headers Optional email headers.
* @param array $attachments Optional file attachments.
* @param int $priority Priority (1-10, lower is higher priority).
* @return int|false Queue ID on success, false on failure.
*/
public static function add_to_queue( $to, $subject, $message, $headers = array(), $attachments = array(), $priority = 5 ) {
global $wpdb;
$result = $wpdb->insert(
self::$table_name,
array(
'recipient' => sanitize_email( $to ),
'subject' => sanitize_text_field( $subject ),
'message' => wp_kses_post( $message ),
'headers' => maybe_serialize( $headers ),
'attachments' => maybe_serialize( $attachments ),
'priority' => absint( $priority ),
'status' => 'pending',
),
array( '%s', '%s', '%s', '%s', '%s', '%d', '%s' )
);
return $result ? $wpdb->insert_id : false;
}
/**
* Process email queue
*
* @return void
*/
public static function process_queue() {
global $wpdb;
// Get pending emails ordered by priority and creation time
$emails = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM " . self::$table_name . "
WHERE status = 'pending'
AND attempts < 3 ORDER BY priority ASC, created_at ASC LIMIT %d", self::$batch_size ) ); if ( empty( $emails ) ) { return; } $sent_count = 0; $error_count = 0; foreach ( $emails as $email ) { // Update attempt count $wpdb->update(
self::$table_name,
array( 'attempts' => $email->attempts + 1 ),
array( 'id' => $email->id ),
array( '%d' ),
array( '%d' )
);
// Send email
$headers = maybe_unserialize( $email->headers );
$attachments = maybe_unserialize( $email->attachments );
$sent = wp_mail(
$email->recipient,
$email->subject,
$email->message,
$headers,
$attachments
);
if ( $sent ) {
// Mark as sent
$wpdb->update(
self::$table_name,
array(
'status' => 'sent',
'sent_at' => current_time( 'mysql' ),
),
array( 'id' => $email->id ),
array( '%s', '%s' ),
array( '%d' )
);
$sent_count++;
} else {
// Mark as failed if max attempts reached
if ( $email->attempts + 1 >= 3 ) {
$wpdb->update(
self::$table_name,
array( 'status' => 'failed' ),
array( 'id' => $email->id ),
array( '%s' ),
array( '%d' )
);
}
$error_count++;
}
// Rate limiting: small delay between sends
usleep( 100000 ); // 0.1 second
}
// Log results
error_log(
sprintf(
'Email Queue Processed: %d sent, %d failed',
$sent_count,
$error_count
)
);
}
/**
* Get queue statistics
*
* @return array Queue statistics.
*/
public static function get_stats() {
global $wpdb;
return array(
'pending' => $wpdb->get_var( "SELECT COUNT(*) FROM " . self::$table_name . " WHERE status = 'pending'" ),
'sent' => $wpdb->get_var( "SELECT COUNT(*) FROM " . self::$table_name . " WHERE status = 'sent'" ),
'failed' => $wpdb->get_var( "SELECT COUNT(*) FROM " . self::$table_name . " WHERE status = 'failed'" ),
);
}
}
// Initialize email queue
Email_Queue_Manager::init();Content Expiration System
Automatically expire posts, pages, or custom post types after a specified period.
<?php
/**
* Content Expiration Manager
*
* Handles automatic expiration of posts with custom expiry dates.
*
* @package YourPlugin
*/
class Content_Expiration_Manager {
/**
* Initialize expiration system
*
* @return void
*/
public static function init() {
add_action( 'init', array( __CLASS__, 'schedule_expiration_check' ) );
add_action( 'check_expired_content', array( __CLASS__, 'check_and_expire_content' ) );
add_action( 'add_meta_boxes', array( __CLASS__, 'add_expiration_meta_box' ) );
add_action( 'save_post', array( __CLASS__, 'save_expiration_date' ), 10, 2 );
}
/**
* Schedule hourly expiration check
*
* @return void
*/
public static function schedule_expiration_check() {
if ( ! wp_next_scheduled( 'check_expired_content' ) ) {
wp_schedule_event( time(), 'hourly', 'check_expired_content' );
}
}
/**
* Check and expire content
*
* @return void
*/
public static function check_and_expire_content() {
$args = array(
'post_type' => 'any',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_query' => array(
array(
'key' => '_expiration_date',
'value' => current_time( 'timestamp' ),
'compare' => '<=', 'type' => 'NUMERIC',
),
),
);
$expired_posts = get_posts( $args );
if ( empty( $expired_posts ) ) {
return;
}
foreach ( $expired_posts as $post ) {
$expiration_action = get_post_meta( $post->ID, '_expiration_action', true );
switch ( $expiration_action ) {
case 'draft':
wp_update_post(
array(
'ID' => $post->ID,
'post_status' => 'draft',
)
);
break;
case 'trash':
wp_trash_post( $post->ID );
break;
case 'delete':
wp_delete_post( $post->ID, true );
break;
case 'private':
wp_update_post(
array(
'ID' => $post->ID,
'post_status' => 'private',
)
);
break;
}
// Log expiration
error_log(
sprintf(
'Content Expired: Post ID %d (%s) - Action: %s',
$post->ID,
$post->post_title,
$expiration_action
)
);
// Clear expiration meta
delete_post_meta( $post->ID, '_expiration_date' );
delete_post_meta( $post->ID, '_expiration_action' );
}
}
/**
* Add expiration meta box
*
* @return void
*/
public static function add_expiration_meta_box() {
add_meta_box(
'content_expiration',
__( 'Content Expiration', 'textdomain' ),
array( __CLASS__, 'render_meta_box' ),
array( 'post', 'page' ),
'side',
'high'
);
}
/**
* Render expiration meta box
*
* @param WP_Post $post Current post object.
* @return void
*/
public static function render_meta_box( $post ) {
wp_nonce_field( 'save_expiration_date', 'expiration_date_nonce' );
$expiration_date = get_post_meta( $post->ID, '_expiration_date', true );
$expiration_action = get_post_meta( $post->ID, '_expiration_action', true );
$date_value = $expiration_date ? gmdate( 'Y-m-d\TH:i', $expiration_date ) : '';
?>
<label for="expiration_date"><?php esc_html_e( 'Expiration Date:', 'textdomain' ); ?></label>
<input type="datetime-local" id="expiration_date" name="expiration_date" value="<?php echo esc_attr( $date_value ); ?>" style="width: 100%;">
<label for="expiration_action"><?php esc_html_e( 'Action on Expiration:', 'textdomain' ); ?></label>
<select id="expiration_action" name="expiration_action" style="width: 100%;">
<option value="draft" <?php selected( $expiration_action, 'draft' ); ?>><?php esc_html_e( 'Move to Draft', 'textdomain' ); ?></option>
<option value="trash" <?php selected( $expiration_action, 'trash' ); ?>><?php esc_html_e( 'Move to Trash', 'textdomain' ); ?></option>
<option value="private" <?php selected( $expiration_action, 'private' ); ?>><?php esc_html_e( 'Make Private', 'textdomain' ); ?></option>
<option value="delete" <?php selected( $expiration_action, 'delete' ); ?>><?php esc_html_e( 'Permanently Delete', 'textdomain' ); ?></option>
</select>
<?php
}
/**
* Save expiration date
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @return void
*/
public static function save_expiration_date( $post_id, $post ) {
// Security checks
if ( ! isset( $_POST['expiration_date_nonce'] ) ) {
return;
}
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['expiration_date_nonce'] ) ), 'save_expiration_date' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// Save expiration date
if ( isset( $_POST['expiration_date'] ) && ! empty( $_POST['expiration_date'] ) ) {
$expiration_date = strtotime( sanitize_text_field( wp_unslash( $_POST['expiration_date'] ) ) );
update_post_meta( $post_id, '_expiration_date', $expiration_date );
} else {
delete_post_meta( $post_id, '_expiration_date' );
}
// Save expiration action
if ( isset( $_POST['expiration_action'] ) ) {
$allowed_actions = array( 'draft', 'trash', 'private', 'delete' );
$action = sanitize_text_field( wp_unslash( $_POST['expiration_action'] ) );
if ( in_array( $action, $allowed_actions, true ) ) {
update_post_meta( $post_id, '_expiration_action', $action );
}
}
}
}
// Initialize content expiration
Content_Expiration_Manager::init();Unscheduling Events
Properly unscheduling events is crucial for plugin deactivation, feature toggling, and preventing orphaned cron jobs that continue running indefinitely.
wp_unschedule_event()
Removes a specific scheduled event. You must provide the exact timestamp and hook name, along with any arguments that were used when scheduling.
/**
* Unschedule a specific event
*
* @return void
*/
function unschedule_specific_backup() {
$timestamp = wp_next_scheduled( 'perform_database_backup' );
$hook = 'perform_database_backup';
$args = array( 'backup_type' => 'full' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, $hook, $args );
}
}wp_clear_scheduled_hook()
Removes all scheduled occurrences of a hook, regardless of arguments or timestamp. This is the preferred method for cleanup during plugin deactivation.
/**
* Clear all scheduled events for a hook
*
* @return void
*/
function clear_all_backup_events() {
wp_clear_scheduled_hook( 'perform_database_backup' );
wp_clear_scheduled_hook( 'cleanup_old_backups' );
}wp_unschedule_hook()
Unschedules all events attached to a specific hook. Available since WordPress 4.9.0.
/**
* Unschedule all events for a hook (WP 4.9+)
*
* @return void
*/
function unschedule_all_sync_events() {
if ( function_exists( 'wp_unschedule_hook' ) ) {
wp_unschedule_hook( 'sync_external_api_data' );
}
}Plugin Deactivation Hook
Always clear scheduled events when your plugin is deactivated to prevent orphaned cron jobs.
/**
* Cleanup on plugin deactivation
*
* @return void
*/
function cleanup_plugin_cron_events() {
// Clear all plugin-specific cron events
$hooks = array(
'perform_daily_database_backup',
'cleanup_old_backups',
'process_email_queue',
'check_expired_content',
'sync_external_api_data',
'warm_site_cache',
'generate_weekly_analytics_report',
);
foreach ( $hooks as $hook ) {
wp_clear_scheduled_hook( $hook );
}
// Clear custom intervals (optional - they'll be re-added on reactivation)
// Note: This doesn't actually remove the intervals from memory
// but prevents confusion if plugin is reactivated
}
register_deactivation_hook( __FILE__, 'cleanup_plugin_cron_events' );Debugging and Monitoring
Debugging WP-Cron can be challenging since events run in the background. These tools and techniques will help you monitor, troubleshoot, and verify cron execution.
Check if WP-Cron is Disabled
<?php
/**
* Check if WP-Cron is disabled
*
* @return bool True if disabled, false otherwise.
*/
function is_wp_cron_disabled() {
return defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
}
/**
* Display admin notice if WP-Cron is disabled
*
* @return void
*/
function wp_cron_disabled_notice() {
if ( is_wp_cron_disabled() ) {
?>
<div class="notice notice-warning">
<?php esc_html_e( 'WP-Cron is disabled. Scheduled events require system cron configuration.', 'textdomain' ); ?>
</div>
<?php
}
}
add_action( 'admin_notices', 'wp_cron_disabled_notice' );List All Scheduled Events
<?php
/**
* Get all scheduled cron events
*
* @return array List of all scheduled events.
*/
function get_all_cron_events() {
$crons = _get_cron_array();
$events = array();
if ( empty( $crons ) ) {
return $events;
}
foreach ( $crons as $timestamp => $cronhooks ) {
foreach ( $cronhooks as $hook => $details ) {
foreach ( $details as $key => $data ) {
$events[] = array(
'hook' => $hook,
'timestamp' => $timestamp,
'schedule' => isset( $data['schedule'] ) ? $data['schedule'] : 'single',
'args' => isset( $data['args'] ) ? $data['args'] : array(),
'time' => gmdate( 'Y-m-d H:i:s', $timestamp ),
'in' => human_time_diff( time(), $timestamp ),
);
}
}
}
return $events;
}
/**
* Display cron events in admin
*
* @return void
*/
function display_cron_events_admin_page() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$events = get_all_cron_events();
?>
<div class="wrap">
<h1><?php esc_html_e( 'Scheduled Cron Events', 'textdomain' ); ?></h1>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th><?php esc_html_e( 'Hook', 'textdomain' ); ?></th>
<th><?php esc_html_e( 'Next Run', 'textdomain' ); ?></th>
<th><?php esc_html_e( 'Schedule', 'textdomain' ); ?></th>
<th><?php esc_html_e( 'Recurrence', 'textdomain' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $events as $event ) : ?>
<tr>
<td><code><?php echo esc_html( $event['hook'] ); ?></code></td>
<td><?php echo esc_html( $event['time'] . ' (' . $event['in'] . ')' ); ?></td>
<td><?php echo esc_html( $event['schedule'] ); ?></td>
<td><?php echo esc_html( $event['schedule'] !== 'single' ? 'Recurring' : 'One-time' ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php
}Manually Trigger Cron Event
<?php
/**
* Manually trigger a cron event for testing
*
* @param string $hook Hook name to execute.
* @return void
*/
function manually_trigger_cron_event( $hook ) {
// Security check
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Insufficient permissions', 'textdomain' ) );
}
// Verify nonce
check_admin_referer( 'trigger_cron_event' );
// Execute the hook
do_action( sanitize_text_field( $hook ) );
wp_safe_redirect( admin_url( 'tools.php?page=cron-manager&triggered=1' ) );
exit;
}
/**
* Add manual trigger button to admin
*
* @param string $hook_name Hook name.
* @return void
*/
function render_manual_trigger_button( $hook_name ) {
$url = wp_nonce_url(
admin_url( 'admin-post.php?action=trigger_cron&hook=' . urlencode( $hook_name ) ),
'trigger_cron_event'
);
?>
<a href="<?php echo esc_url( $url ); ?>" class="button button-secondary">
<?php esc_html_e( 'Run Now', 'textdomain' ); ?>
</a>
<?php
}
add_action( 'admin_post_trigger_cron', 'manually_trigger_cron_event' );Cron Execution Logger
/**
* Cron Event Logger
*
* Logs all cron executions to custom database table.
*
* @package YourPlugin
*/
class Cron_Event_Logger {
/**
* Log table name
*
* @var string
*/
private static $table_name = '';
/**
* Initialize logger
*
* @return void
*/
public static function init() {
global $wpdb;
self::$table_name = $wpdb->prefix . 'cron_log';
add_action( 'plugins_loaded', array( __CLASS__, 'create_table' ) );
add_action( 'all', array( __CLASS__, 'log_cron_execution' ), 10, 0 );
}
/**
* Create log table
*
* @return void
*/
public static function create_table() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS " . self::$table_name . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
hook varchar(255) NOT NULL,
execution_time datetime DEFAULT CURRENT_TIMESTAMP,
duration float DEFAULT NULL,
status varchar(20) DEFAULT 'success',
memory_usage bigint(20) DEFAULT NULL,
PRIMARY KEY (id),
KEY hook (hook),
KEY execution_time (execution_time)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta( $sql );
}
/**
* Log cron execution
*
* @return void
*/
public static function log_cron_execution() {
$current_filter = current_filter();
// Only log known cron hooks
$cron_hooks = array(
'perform_daily_database_backup',
'process_email_queue',
'check_expired_content',
'sync_external_api_data',
);
if ( ! in_array( $current_filter, $cron_hooks, true ) ) {
return;
}
$start_time = microtime( true );
$start_memory = memory_get_usage();
// Let the event execute
// We use a shutdown hook to record completion
add_action(
'shutdown',
function() use ( $current_filter, $start_time, $start_memory ) {
global $wpdb;
$duration = microtime( true ) - $start_time;
$memory_usage = memory_get_usage() - $start_memory;
$wpdb->insert(
Cron_Event_Logger::$table_name,
array(
'hook' => $current_filter,
'duration' => $duration,
'memory_usage' => $memory_usage,
'status' => 'success',
),
array( '%s', '%f', '%d', '%s' )
);
}
);
}
/**
* Get execution stats for a hook
*
* @param string $hook Hook name.
* @param int $limit Number of recent executions to retrieve.
* @return array Execution statistics.
*/
public static function get_hook_stats( $hook, $limit = 10 ) {
global $wpdb;
return $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM " . self::$table_name . "
WHERE hook = %s
ORDER BY execution_time DESC
LIMIT %d",
$hook,
$limit
)
);
}
}
// Initialize logger
Cron_Event_Logger::init();Replacing WP-Cron with System Cron
For high-traffic sites or sites requiring precise timing, replacing WP-Cron with system cron provides better performance and reliability. This involves disabling WP-Cron and configuring a server-level cron job to trigger wp-cron.php at regular intervals.
Step 1: Disable WP-Cron
Add this constant to your wp-config.php file before the “That’s all, stop editing!” line:
define( 'DISABLE_WP_CRON', true );Step 2: Configure System Cron
Add a cron job to your server that runs every minute (or your preferred interval). Access your server’s crontab with crontab -e and add:
# Run WordPress cron every minute
* * * * * cd /path/to/wordpress && php wp-cron.php > /dev/null 2>&1
# Alternative using curl
* * * * * curl -s https://yourdomain.com/wp-cron.php > /dev/null 2>&1
# Alternative using wget
* * * * * wget -q -O - https://yourdomain.com/wp-cron.php > /dev/null 2>&1Step 3: Verify System Cron is Working
<?php
/**
* Admin notice to verify system cron configuration
*
* @return void
*/
function verify_system_cron_notice() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
if ( ! defined( 'DISABLE_WP_CRON' ) || ! DISABLE_WP_CRON ) {
return;
}
// Check if wp-cron.php was recently accessed
$last_cron_run = get_transient( 'last_cron_run_time' );
if ( ! $last_cron_run || ( time() - $last_cron_run ) > 120 ) {
?>
<div class="notice notice-error">
<strong><?php esc_html_e( 'System Cron Not Running!', 'textdomain' ); ?></strong>
<?php esc_html_e( 'WP-Cron is disabled but system cron does not appear to be configured correctly.', 'textdomain' ); ?>
</div>
<?php
}
}
add_action( 'admin_notices', 'verify_system_cron_notice' );
/**
* Update last cron run time
*
* @return void
*/
function update_last_cron_run() {
set_transient( 'last_cron_run_time', time(), 300 );
}
add_action( 'wp_cron', 'update_last_cron_run' );Benefits of System Cron
- Precise Timing: Events run at exact intervals regardless of traffic
- Better Performance: No overhead from checking cron on every page load
- Reliability: Works on low-traffic sites without delays
- Resource Control: Dedicated execution window prevents overlap
Best Practices and Security
Always Check if Event is Already Scheduled
Before scheduling an event, always verify it’s not already scheduled to prevent duplicate executions.
// GOOD: Check before scheduling
if ( ! wp_next_scheduled( 'my_custom_event' ) ) {
wp_schedule_event( time(), 'hourly', 'my_custom_event' );
}
// BAD: Schedule without checking (creates duplicates)
wp_schedule_event( time(), 'hourly', 'my_custom_event' );Use Unique Hook Names
Prefix your hook names with your plugin/theme slug to avoid conflicts.
// GOOD: Unique, prefixed hook name
add_action( 'init', 'myplugin_schedule_events' );
function myplugin_schedule_events() {
if ( ! wp_next_scheduled( 'myplugin_daily_cleanup' ) ) {
wp_schedule_event( time(), 'daily', 'myplugin_daily_cleanup' );
}
}
// BAD: Generic hook name (risk of conflicts)
wp_schedule_event( time(), 'daily', 'daily_cleanup' );Implement Proper Error Handling
/**
* Safe cron execution with error handling
*
* @return void
*/
function safe_cron_task_execution() {
try {
// Attempt risky operation
$result = perform_risky_operation();
if ( is_wp_error( $result ) ) {
error_log( 'Cron Error: ' . $result->get_error_message() );
return;
}
// Success logging
update_option( 'last_successful_cron_run', current_time( 'timestamp' ) );
} catch ( Exception $e ) {
error_log( 'Cron Exception: ' . $e->getMessage() );
// Optionally notify admin
wp_mail(
get_option( 'admin_email' ),
'Cron Task Failed',
'Error: ' . $e->getMessage()
);
}
}
add_action( 'my_cron_hook', 'safe_cron_task_execution' );Limit Execution Time for Long Tasks
/**
* Process large dataset in chunks
*
* @return void
*/
function process_large_dataset_in_chunks() {
$batch_size = 100;
$max_execution = 30; // seconds
$start_time = time();
$offset = get_option( 'dataset_processing_offset', 0 );
while ( ( time() - $start_time ) < $max_execution ) {
$items = get_items_to_process( $offset, $batch_size );
if ( empty( $items ) ) {
// Processing complete
delete_option( 'dataset_processing_offset' );
break;
}
foreach ( $items as $item ) {
process_single_item( $item );
}
$offset += $batch_size;
update_option( 'dataset_processing_offset', $offset );
}
}Secure Cron Endpoints
If exposing custom cron endpoints, always implement authentication.
/**
* Secure custom cron endpoint
*
* @return void
*/
function secure_cron_endpoint() {
// Verify secret key
if ( ! isset( $_GET['key'] ) || $_GET['key'] !== get_option( 'cron_secret_key' ) ) {
wp_die( esc_html__( 'Unauthorized', 'textdomain' ), 403 );
}
// Verify nonce for additional security
if ( ! isset( $_GET['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['nonce'] ) ), 'custom_cron_action' ) ) {
wp_die( esc_html__( 'Invalid request', 'textdomain' ), 403 );
}
// Execute cron task
do_action( 'custom_secure_cron_task' );
wp_die( 'Success', 200 );
}
add_action( 'admin_post_nopriv_custom_cron', 'secure_cron_endpoint' );Clean Up on Plugin Deactivation
Always unschedule events when deactivating plugins to prevent orphaned cron jobs.
/**
* Plugin deactivation cleanup
*
* @return void
*/
function myplugin_deactivation_cleanup() {
// Clear all plugin cron events
wp_clear_scheduled_hook( 'myplugin_daily_cleanup' );
wp_clear_scheduled_hook( 'myplugin_hourly_sync' );
wp_clear_scheduled_hook( 'myplugin_weekly_report' );
// Clear any stored cron-related options
delete_option( 'myplugin_last_cron_run' );
delete_option( 'myplugin_cron_errors' );
}
register_deactivation_hook( __FILE__, 'myplugin_deactivation_cleanup' );Common Pitfalls to Avoid
1. Scheduling Events on Every Page Load
Problem: Scheduling events in actions like init without checking if already scheduled creates duplicate events.
// WRONG: Creates duplicate events on every page load
add_action( 'init', 'bad_scheduling_example' );
function bad_scheduling_example() {
wp_schedule_event( time(), 'hourly', 'my_hourly_task' );
}
// CORRECT: Check before scheduling
add_action( 'init', 'good_scheduling_example' );
function good_scheduling_example() {
if ( ! wp_next_scheduled( 'my_hourly_task' ) ) {
wp_schedule_event( time(), 'hourly', 'my_hourly_task' );
}
}2. Not Passing Arguments Correctly
Problem: Arguments must match exactly when unscheduling or checking for scheduled events.
// Schedule with arguments
wp_schedule_event( time(), 'hourly', 'process_user_data', array( 'user_id' => 42 ) );
// WRONG: Won't find the event (missing arguments)
$next = wp_next_scheduled( 'process_user_data' );
// CORRECT: Include the same arguments
$next = wp_next_scheduled( 'process_user_data', array( 'user_id' => 42 ) );3. Assuming Immediate Execution
Problem: WP-Cron doesn’t guarantee immediate execution. Events run when triggered by page loads after the scheduled time.
// Don't expect immediate execution
wp_schedule_single_event( time() + 60, 'send_notification' );
// The notification won't send in exactly 60 seconds
// It sends when someone visits the site after 60 seconds
// For immediate execution, use direct function calls
send_notification();
// Or for background processing, use wp_remote_post with blocking => false4. Long-Running Tasks Blocking Execution
Problem: Cron tasks that take too long can cause timeouts or block subsequent events.
// WRONG: Processing millions of records at once
function process_all_users() {
$users = get_users( array( 'number' => -1 ) ); // Gets ALL users
foreach ( $users as $user ) {
heavy_processing( $user );
}
}
// CORRECT: Process in batches
function process_users_in_batches() {
$batch_size = 50;
$offset = get_option( 'user_processing_offset', 0 );
$users = get_users(
array(
'number' => $batch_size,
'offset' => $offset,
)
);
if ( empty( $users ) ) {
delete_option( 'user_processing_offset' );
return;
}
foreach ( $users as $user ) {
heavy_processing( $user );
}
update_option( 'user_processing_offset', $offset + $batch_size );
}5. Not Validating Data Before Processing
Problem: Cron tasks run without user interaction, so they must validate all data.
// WRONG: No validation
function process_scheduled_post( $post_id ) {
$post = get_post( $post_id );
wp_publish_post( $post_id ); // Could fail if post was deleted
}
// CORRECT: Validate before processing
function process_scheduled_post_safe( $post_id ) {
$post = get_post( $post_id );
// Validate post exists
if ( ! $post ) {
error_log( "Cron: Post {$post_id} not found" );
return;
}
// Validate post is in expected status
if ( 'draft' !== $post->post_status ) {
error_log( "Cron: Post {$post_id} is not a draft" );
return;
}
// Proceed with publishing
wp_publish_post( $post_id );
}6. Forgetting to Remove Transient Checks
Problem: Using transients to prevent duplicate execution but never clearing them.
// WRONG: Transient never cleared
function run_once_daily() {
if ( get_transient( 'daily_task_running' ) ) {
return;
}
set_transient( 'daily_task_running', true, HOUR_IN_SECONDS );
perform_daily_task();
// Transient not deleted - prevents future runs if task fails
}
// CORRECT: Always clear the lock
function run_once_daily_safe() {
if ( get_transient( 'daily_task_running' ) ) {
return;
}
set_transient( 'daily_task_running', true, HOUR_IN_SECONDS );
try {
perform_daily_task();
} finally {
delete_transient( 'daily_task_running' );
}
}Performance Optimization
Optimize Database Queries
/**
* Optimized cron task with efficient queries
*
* @return void
*/
function optimized_cleanup_task() {
global $wpdb;
// Use direct SQL for bulk operations instead of get_posts()
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->posts}
WHERE post_type = 'revision'
AND post_date < %s LIMIT 1000", gmdate( 'Y-m-d', strtotime( '-30 days' ) ) ) ); // Clean up orphaned meta $wpdb->query(
"DELETE pm FROM {$wpdb->postmeta} pm
LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.ID IS NULL"
);
// Clear object cache
wp_cache_flush();
}Use Transients for Rate Limiting
/**
* Rate-limited API sync with transient
*
* @return void
*/
function rate_limited_api_sync() {
// Check if we've synced recently
if ( get_transient( 'api_sync_cooldown' ) ) {
return; // Skip this run
}
// Perform sync
$result = sync_with_external_api();
if ( $result ) {
// Set cooldown period (5 minutes)
set_transient( 'api_sync_cooldown', true, 5 * MINUTE_IN_SECONDS );
}
}Implement Progress Tracking
/**
* Track progress of long-running tasks
*
* @return void
*/
function tracked_batch_processor() {
$total_items = get_option( 'batch_total_items', 0 );
$processed = get_option( 'batch_processed_items', 0 );
$batch_size = 50;
if ( $processed >= $total_items ) {
// All done
delete_option( 'batch_total_items' );
delete_option( 'batch_processed_items' );
return;
}
// Process next batch
$items = get_items_batch( $processed, $batch_size );
foreach ( $items as $item ) {
process_item( $item );
$processed++;
}
update_option( 'batch_processed_items', $processed );
// Calculate progress
$progress = ( $processed / $total_items ) * 100;
update_option( 'batch_progress', round( $progress, 2 ) );
}Memory Management
/**
* Memory-efficient large dataset processing
*
* @return void
*/
function memory_efficient_processing() {
global $wpdb;
$batch_size = 100;
$offset = 0;
do {
// Process in small chunks
$posts = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_title FROM {$wpdb->posts}
WHERE post_status = 'publish'
AND post_type = 'post'
LIMIT %d OFFSET %d",
$batch_size,
$offset
)
);
if ( empty( $posts ) ) {
break;
}
foreach ( $posts as $post ) {
process_post( $post );
}
// Free memory
unset( $posts );
wp_cache_flush();
$offset += $batch_size;
// Memory threshold check
if ( memory_get_usage() > ( 50 * 1024 * 1024 ) ) { // 50MB
error_log( 'Cron: Memory threshold reached, stopping batch' );
break;
}
} while ( true );
}