WordPress at Your Fingertips

WordPress Cron (task scheduler)

Suppose we need some PHP function to be executed after 5 hours. Or we need this function to run every day. The WordPress Cron - Task Scheduler will help to solve such problems. Below, let's understand how it works and how to use it.

The name Cron is taken from UNIX-like operating systems. There, Cron is a task scheduler responsible for periodically performing the specified actions at a certain time.

See on the subject:

WordPress Cron Tasks (out of the box)

WordPress uses the cron for its own needs, so it has basic Cron tasks:

wp_version_check → wp_version_check()
wp_update_plugins → wp_update_plugins()
wp_update_themes → wp_update_themes()
twice a day (twicedaily)
Update check for plugins, themes, translations, and core.
wp_scheduled_delete → wp_scheduled_delete()
once a day (daily)
Delete comments and posts from the trash.
delete_expired_transients → delete_expired_transients()
once a day (daily)
Delete expired transient options.
wp_scheduled_auto_draft_delete → wp_delete_auto_drafts()
once a day (daily)
Delete auto-drafts.
wp_privacy_delete_old_export_files → wp_privacy_delete_old_export_files()
once a day (daily) (since WP 4.9.6)
Delete old export files.
recovery_mode_clean_expired_keys → clean_expired_keys()
once a day (daily) (since WP 5.2) (not for multisite)
Delete expired recovery mode keys.
wp_site_health_scheduled_check → WP_Site_Health::wp_cron_scheduled_check()
once a week (weekly) (since WP 5.4)
Check and update site health status.
wp_https_detection → wp_update_https_detection_errors()
twice a day (twicedaily) (since WP 5.7)
Check if the site supports HTTPS.
do_pings → do_all_enclosures()
One-time event, created when a post is created/updated with the status publish.
Process enclosures for all posts.
wp_update_user_counts → wp_schedule_update_user_counts().
twice a day (twicedaily) (since WP 6.0)
Update user count data. Does not work for multisite, a different task update_network_counts works there.
wp_delete_temp_updater_backups → wp_delete_all_temp_backups()
once a week (weekly) (since WP 6.3).
Delete the contents of the temporary directory /wp-content/upgrade-temp-backup, which is used for creating a backup when updating WordPress.
wp_update_comment_type_batch → _wp_batch_update_comment_type()
One-time task (since WP 5.5).
Update comment types. Since version 5.5, the default comment type has changed from '' to 'comment'. This task updates all comment types in the database.

Multisite

update_network_counts → wp_update_network_counts()
twice a day (twicedaily)
Update counts for the multisite network - the number of users and the number of blogs in the network.

How WordPress Cron Works (Step by Step)

  1. When you visit any page of the site (any request to the site), including AJAX, REST requests, ie always on the init action, triggers the wp_cron() function.

    if ( ! defined( 'DOING_CRON' ) )
    	add_action( 'init', 'wp_cron' );
  2. wp_cron() registers the launch of _wp_cron() on the [wp_loaded] action(/handbook/wp-cli/hook/wp_loaded).

  3. _wp_cron() checks the DISABLE_WP_CRON constant:

    • if it is not set, then checks if there is at least one job with an appropriate time. If there is, it calls the spawn_cron() function.
    • if it is set, then all cron code aborted.
  4. spawn_cron() launches cron! Sends non-blocking HTTP request to cron file /wp-cron.php in which it sends current timestamp of the form: microtime(true), for example: 1538326680.8330409526824951171875.

    This checks when was the last time the cron was started, if less than 60 seconds, the function does nothing. This interval can be changed via the WP_CRON_LOCK_TIMEOUT constant. You can specify a maximum of 600 (10 minutes) in the constant, if you specify more, it will be ignored. Example:

    define( 'WP_CRON_LOCK_TIMEOUT', 60 )

    Next, it checks if there is at least one matching job, if there is, the "when cron was started" timestamp is written in the transient option doing_cron . Then a non-blocking request is sent to the file /wp-cron.php, there this timestamp is used for various checks.

  5. /wp-cron.php file checks again: whether the cron was started recently (less than 60 sec ago), whether there are any matching cron jobs.

    Next, it goes through all the "suitable" tasks and for each of them: deletes the current task from the schedule, and if the task has a tag schedule, (it is recurring), creates a new task of the same kind using the function wp_reschedule_event(). The new task will be executed after the time interval specified in the interval.

    Next, triggers the wp hook specified for the task during cron job registration, i.e. the current task is started:

    do_action_ref_array( $hook, $v['args'] );

    If you access the file /wp-cron.php directly, it will only work once every WP_CRON_LOCK_TIMEOUT seconds (60 seconds).

    How long tasks are handled. Suppose there are several tasks in a cron. The "first" cron request was sent and the first task started to run. But it is running so long (more than 60 seconds) that a "second" cron request was sent. In this case, next task in first cron request will NOT be executed - first request will simply stop after "long" task is executed. Next cron job will be executed in second request.

Notes

All Cron tasks are stored in the get_option( 'cron' ) option.

The Cron request runs separately from the current page load and it separately loads the WP environment. The current request (page visit) only initializes the Cron (creates a request for a Cron file) if the time is up.

In other words: Cron tasks are executed asynchronously. That is, to execute the pending Cron tasks, WordPress sends a request to the http://example.com/wp-cron.php file. This file is self-sufficient: it sets the define('DOING_CRON', true) constant, then loads the WordPress environment and executes all pending tasks.

Make Sure the WordPress Cron Works

Option 1

This can be done on the Tools → Site Health admin page. Go to the page and wait a bit (while waiting WP will try to make a "loopback request") if it fails, you will see the following message and it means that the WP-Cron does not work.

In site health you will NOT see errors if Cron is turned off intentionally, via the DISABLE_WP_CRON constant.

Option 2

  1. Install plugin WP Crontrol. (You can uninstall it after checking).

  2. Go to the Tools → Cron Events page and if the Cron is not working you will see warnings that some tasks are overdue:

Option 3

Publish a post with a date later than the current date (it will be queued for publication) and see if it gets published or not when the time is up.

If the cron is working, the post will be published at the specified time.

Reasons WP-Cron may not work

The Сron can fail for a variety of reasons:

  • It is turned off intentionally. The DISABLE_WP_CRON configuration constant is set but no alternative cron runner has been put in place.
  • Your server is not capable of sending HTTP requests. The cron is activated through a non-blocking WP HTTP request to itself.
  • A plugin may intentionally or unintentionally break the event runner.
  • A fatal error caused by a plugin or theme may break the event runner.

More reasons see here

Alternate Cron launch option

By default, the WP cron is triggered by a non-blocking POST request from PHP, using wp_remote_post(). Such a request is created from any visited page of the site, if the WP cron job execution time is up.

If this option does not work on your server for some reason, you can enable an alternative option to run the cron job.

To do this, add such a constant to the wp-config.php file:

define( 'ALTERNATE_WP_CRON', true );

This cron run will create a request via wp_redirect(), i.e. the request will not be sent from PHP, but by the client (browser).

The user's browser will receive the redirect when the cron needs to be run. Thus, it will immediately return to the site, while the cron will continue to run. This method has some risks because it depends on a non-native WordPress service.

For more details, see. spawn_cron().

Trigger WP-cron from the server only

  1. Disable WP-Cron. Add define( 'DISABLE_WP_CRON', true ); into wp-config.php. Read below.

  2. Add a Cron job to your server. It must send GET request to the /wp-cron.php WordPress file. Read below.

How Turn Off the WP-Cron?

To disable the cron, you need to go into the file wp-config.php and add the following line there:

define( 'DISABLE_WP_CRON', true );

This option disables cron initialization in WP. At that, if you send direct request to /wp-cron.php file, all matured tasks will be executed as if cron was working.

It is strongly recommended to NOT disable WP-Cron, because WordPress uses it to clean up all sorts of drafts and deletes posts from the Recycle Bin, and some plugins may also work on the basis of WP-Cron!

WP-Cron Functions

Adding:

wp_schedule_event( $timestamp, $recurrence, $hook, $args, $wp_error )
Schedule a recurring event.
wp_schedule_single_event( $timestamp, $hook, $args, $wp_error )
Schedules an event to run only once.

Deleting:

wp_unschedule_event( $timestamp, $hook, $args, $wp_error )
Unschedule a previously scheduled event.
wp_clear_scheduled_hook( $hook, $args, $wp_error )
Unschedules all events attached to the hook with the specified arguments.
wp_unschedule_hook( $hook, $wp_error )
Unschedules all events attached to the hook.

Others:

wp_next_scheduled( $hook, $args )
Retrieve the next timestamp for an event.
wp_doing_cron( )
Determines whether the current request is a WordPress cron request.
wp_get_schedules( )
Retrieve supported event recurrence schedules.

See the entire list at this link.

Creating WP-Cron Jobs

One of the functions is used to create new WP-Cron job:

You need to call the function to create a cron job only once! Usually this is done when the plugin is activated, and cron tasks must be deleted when it is deactivated. Or you can first check if there is no already specified cron task, if not, then add it.

When you call the task registration function, it is written to the site option cron and works from there autonomously.

If task is in the cron, but it does not work, it means that the wp-hook is not connected during the cron request.

For example, if you register the Cron-task with AJAX and add the Cron hook in the file which is included only on AJAX request, then the Cron task will be registered, but the function at the right time will not be executed. It happens because there won't be corresponding wp-hook to be run when WP-Cron task is appears!

Therefore, the hook itself needs to be placed in functions.php or in the plugin or something else where it will be added every time WordPress runs, but not while processing ajax request.

If when adding a cron-task, there is already a task with the same name and arguments, and the time of the new task does not exceed ten minutes of the time of the existing one, then adding a new cron-task will be ignored.

The point of this is to prevent scheduling two identical events within ten minutes of each other, but not to prevent scheduling identical events with a time difference of more than ten minutes from each other.

Events are considered identical if the following are the same: the name of the hook and the parameters to be passed.

Repeating Tasks

#1 Create a cron task when activating a plugin

Let's schedule an hourly action for the plugin. To do this, call wp_schedule_event() at plugin activation (if we do not do it at activation, we will get a lot of scheduled events!).

register_activation_hook(__FILE__, 'my_activation');

function my_activation() {
	// let's delete all the same cron tasks, just in case,
	// so we can add new ones from scratch. This might be useful,
	// if the same cron task was connected incorrectly before
	// (without checking if it already exists)
	wp_clear_scheduled_hook( 'my_hourly_event' );

	// check if there is already a task with the same hook
	// this point is unnecessary, because we have deleted all tasks above.
	// if( ! wp_next_scheduled( 'my_hourly_event' ) )

	// add a new cron task
	wp_schedule_event( time(), 'hourly', 'my_hourly_event');
}

add_action( 'my_hourly_event', 'do_this_hourly' );

function do_this_hourly() {
	// do something every hour
}

// When deactivating a plugin, it is obligatory to delete a task:
register_deactivation_hook( __FILE__, 'my_deactivation' );

function my_deactivation(){
	wp_clear_scheduled_hook( 'my_hourly_event' );
}

#2 Create a cron task if it doesn't already exist

This example doesn't rely on activating a plugin (via the plugins directory), instead it adds an event if it doesn't exist.

// adds a new cron task
add_action( 'admin_head', 'my_activation' );

function my_activation() {
	if( ! wp_next_scheduled( 'my_hourly_event' ) ) {
		wp_schedule_event( time(), 'hourly', 'my_hourly_event');
	}
}

// add a function to the specified hook
add_action( 'my_hourly_event', 'do_this_hourly' );

function do_this_hourly(){
	// Do something every hour
}

The disadvantage of this code is that the check always happens for all queries, but the task is added only once. However, the PHP cost here is negligible, comparable to just getting the get_option() option, so you can ignore this for the sake of convenience.

#3 More examples

See function description wp_schedule_event().

One-Time Tasks

#1 Schedule an action in one hour from now

// Adds a new one-time cron task
add_action( 'admin_head', 'my_activation' );

function my_activation() {
	if( ! wp_next_scheduled( 'my_new_event' ) ) {
		wp_schedule_single_event( time() + 3600, 'my_new_event' );
		// time() + 3600 = 1 hour from the current moment.
	}
}

add_action( 'my_new_event','do_this_in_an_hour' );

function do_this_in_an_hour(){
	// делаем что-нибудь
}

#2 More examples

See function description wp_schedule_single_event().

Intervals for WP-Cron Tasks

Cron intervals are needed so that it can be used when re-registering a cron task.

By default in WP three intervals:

'hourly'     => [ 'interval' => HOUR_IN_SECONDS,      'display' => __( 'Once Hourly' ) ],
'twicedaily' => [ 'interval' => 12 * HOUR_IN_SECONDS, 'display' => __( 'Twice Daily' ) ],
'daily'      => [ 'interval' => DAY_IN_SECONDS,       'display' => __( 'Once Daily' ) ],

To add a new Cron interval, use the filter cron_schedules.

Let's add a "5 minute" interval (do something every 5 minutes):

// register the 5-minute interval
add_filter( 'cron_schedules', 'cron_add_five_min' );

function cron_add_five_min( $schedules ) {

	$schedules['five_min'] = array(
		'interval' => 60 * 5,
		'display' => 'Every 5 minutes'
	);

	return $schedules;
}

The interval creation code should always be triggered, because interval data is not stored in the database, but is used all the time. When you perform one cron job, WP creates the same job, and the timestamp of this new job is calculated based on the specified interval name. And so the interval data is always needed!

It would seem why not just specify the interval when adding a task and that's it, why do we need to add the interval through a filter. The point is that the same interval can be used by different cron tasks.

You can now use this interval when creating a cron-task:

// Register the event
add_action( 'wp', 'my_activation' );

function my_activation() {

	if ( ! wp_next_scheduled( 'my_five_min_event' ) ){
		wp_schedule_event( time(), 'five_min', 'my_five_min_event' );
	}
}

// cron job function
add_action( 'my_five_min_event', 'do_every_five_min' );

function do_every_five_min(){
	// doing something every 5 minutes
}

WordPress constants that may come in handy when creating a cron interval:

  • HOUR_IN_SECONDS - 60*60 = 3600
  • DAY_IN_SECONDS - 60*60*24 = 86400
  • WEEK_IN_SECONDS - 60*60*24*7 = 604800

Deleting Cron Tasks

There are 3 functions for deleting WP-Cron tasks:

wp_unschedule_event( $timestamp, $hook, $args )

Deletes a specific cron job. To delete, you need to know all 3 parameters: timestamp, hook, passed parameters.

wp_unschedule_event( 1540722222, 'publish_future_post', array(9227) );

// or like this
$timestamp = wp_next_scheduled( 'my_schedule_hook', array(9227) );
wp_unschedule_event( $timestamp, 'my_schedule_hook', array(9227) );

Note: if the parameter is not passed, it can be omitted accordingly.

wp_clear_scheduled_hook( $hook, $args )

Deletes all cron-tasks attached to the specified hook and having the specified parameters.

wp_clear_scheduled_hook( 'publish_future_post', array(9227) );
wp_unschedule_hook( $hook ).

Removes absolutely all cron-tasks attached to the specified hook, no matter what timestamp or what parameters are passed.

wp_unschedule_hook( 'publish_future_post' );

A visual representation of how the functions work:

Debug WP-Cron in WordPress

Cron tasks are executed asynchronously, so you will not see anything when site pages are loaded, even if you display something in the cron function, for example like this die('stop cron').

To debug the cron function, you need to directly access the http://example.com/wp-cron.php file. Or you can test the cron function separately, and then just hang it on the cron hook and make sure the hook triggered at the right time.

To see the errors (if any), you need to put a smaller interval (to catch the execution of task) and go to the already mentioned link http://example.com/wp-cron.php.

By accessing the wp-cron.php file directly, you will forcibly initialize the running of WordPress cron and see what your functions return, if it's time for their execution. This will include seeing PHP errors if constant WP_DEBUG is enabled in wp-config.php.

It may be useful to temporarily disable the cron so that no cron tasks are executed on the site while you are working on the single cron function.

To disable cron, add this constant to wp-config.php:

define( 'DISABLE_WP_CRON', true );

You can get the entire list of current Cron tasks with the function _get_cron_array():

print_r( _get_cron_array() );

/*
Array
	[1538467983] => Array
			[wp_update_themes] => Array
					[40cd750bba9870f18aada2478b24840a] => Array
							[schedule] => twicedaily
							[args] => Array()
							[interval] => 43200

			[wp_version_check] => Array
					[40cd750bba9870f18aada2478b24840a] => Array
							[schedule] => twicedaily
							[args] => Array()
							[interval] => 43200

			[wp_update_plugins] => Array
					[40cd750bba9870f18aada2478b24840a] => Array
							[schedule] => twicedaily
							[args] => Array()
							[interval] => 43200

	[1538468836] => Array
			[wp_scheduled_delete] => Array
					[40cd750bba9870f18aada2478b24840a] => Array
							[schedule] => daily
							[args] => Array()
							[interval] => 86400

	[1538469436] => Array
			[wp_scheduled_auto_draft_delete] => Array
					[40cd750bba9870f18aada2478b24840a] => Array
							[schedule] => daily
							[args] => Array()
							[interval] => 86400

	[1538474556] => Array
			[delete_expired_transients] => Array
					[40cd750bba9870f18aada2478b24840a] => Array
							[schedule] => daily
							[args] => Array()
							[interval] => 86400

	[1540722222] => Array
			[publish_future_post] => Array
					[21966ef2cb5e27dd021560702f4ee618] => Array
							[schedule] =>
							[args] => Array
									[0] => 9227

	[1540711111] => Array
			[publish_future_post] => Array
					[21966ef2cb5e27dd021560702f4ee618] => Array
							[schedule] =>
							[args] => Array
									[0] => 9227

	[1540722222] => Array
			[publish_future_post] => Array
					[21966ef2cb5e27dd021560702f4ee618] => Array
							[schedule] =>
							[args] => Array
									[0] => 25

	[version] => 2
*/

Trigger Cron from Server

In WP, the cron runs unstable and you can not, for example, specify the exact time of startup. It happens this way because to initialize next cron task, someone (probably a bot) needs to visit the site. Only when someone visits the site, the cron is checked and starts if the time is right. So, if no one visits the site for a month, the cron will not work for a month...

You may solve this problem by creating a cron job on server, which will be executed at right time or every 10 minutes, for example. This job should visit the cron initialization file http://example.com/wp-cron.php. For example you can add such command on the server cron:

# execute command every 5 minutes
*/5 * * * * wget -O /dev/null -q 'https://example.com/wp-cron.php'

# execute command every hour
* */1 * * * wget -O /dev/null -q 'https://example.com/wp-cron.php'

In this case, WP's cron tasks will work even if the site is not visited at the right time.

Crontab linux format, more details read here:

* * * * * The command that will be executed
| | | | |
| | | | └─ Day of the week (0 - 7) (Sunday = 0)
| | | └─── Month (1 - 12)
| | └───── Day of the month (1 - 31)
| └─────── The Hour (0 - 23)
└───────── Minute (0 - 59)

php-cli run issue
The /wp-cron.php file is designed to run via http. So if, for example, you run it through php-cli by directly accessing it, then nothing will work, at least because the code includes processing of the $_SERVER['HTTP_HOST'] variable, and its value in php-cli environment is most likely not what it should be, there will usually be local-cli.

WP-Cron vs Server Cron

The first difference

WordPress cron is only executed when someone visits your site and triggers a page load. This means that WordPress requires an HTTP/HTTPS request from your website to run scheduled tasks.

Why did WordPress choose this particular option to run Cron?

Many WordPress hosting providers only offer shared hosting, which usually means your host won't give you access to the cron command. If they did, you'd have access to the server commands and all data on the server would be potentially compromised. WordPress Cron is therefore the workaround for this problem.

The second difference

When using server Cron, you can set exact time (for example, 17:05 daily to run a task). In WP-Cron, on the other hand, you set intervals (for example, 14:00 & interval X after that). WP-Cron then executes the task at those intervals as long as a user has visited site.

Plugins for controlling the Cron tasks

  • WP Crontrol — A great plugin for viewing and managing WordPress cron tasks.
2 comments
    Log In