One extremely confusing set of functions within WordPress is the concept of “wp_cron
“. For those of you unfamiliar with it, is a scheduling tool on Unix systems and, in itself, is not without its own confusing elements. Let’s take a more user-friendly example for scheduling an event first. Let’s say I wanted to create a recurring event in a calendar app. Probably I’d start by picking a day on a calendar and then put all my information in there, including the recurrence of the event.
On Windows, the task scheduler is a pretty similar workflow. You get a graphical interface, tell it what you want to run and how frequently and, barring some horrible problem, the machine will fire that action or run that program at the desired frequency. This is especially handy for scheduling things like backups.
On Linux-based systems (which is what most webservers run), you have the same level of control (more even), but lack the intuitive user interface. Instead you get this:
* * * * * command to be executed - - - - - | | | | | | | | | +----- day of week (0 - 6) (Sunday=0) | | | +------- month (1 - 12) | | +--------- day of month (1 - 31) | +----------- hour (0 - 23) +------------- min (0 - 59)
This can be incredibly intimidating the first time you see it, and even more so if you’re trying to do, like in the Windows example, something critical like run scheduled backups.
Luckily, WordPress doesn’t use cron
!
Thankfully, in WordPress, you don’t need to memorize what each position in a Linux cron
schedule is, but that’s not to say that WordPress is without its own confusion around the internal task scheduler. If you’ve heard the function name wp_cron
thrown around, you might be inclined to look it up. Which would give you this:
Okay, so there’s some function called wp_cron
, and it’s invoked like that and…it takes no parameters. Um…I think I’ve got that.
Then you look further.
Wait, so there’s no return values and…the examples don’t even use wp_cron
???
Seriously, that just left me with more questions than answers. What’s going on?
Breaking wp_cron
down
Let’s put aside the wp_cron
part of the wp_cron
Codex page, and focus on what’s happening in the example code. The first half of the example already has some new concepts we need to figure out:
if ( ! wp_next_scheduled( 'my_task_hook' ) ) { wp_schedule_event( time(), 'hourly', 'my_task_hook' ); }
There are two things going on here. The first is a check for wp_next_scheduled
. If that fails, we run wp_schedule_event
. Both things take some parameter that matches (the lone parameter passed to wp_next_scheduled
and the third parameter in wp_schedule_event
). I don’t know what either of these things is, but obviously they are working together–time to hit the Codex again.
Okay, so wp_next_scheduled
just returns a timestamp for the next scheduled occurrence for an event. If there’s no scheduled event, it returns false, and we continue scheduling our event. That uses another new function, wp_schedule_event
.
This is the function you use to actually schedule your event or task. The $timestamp
parameter is the time for the first occurrence of this event (usually you would just use the current time with time()
). $recurrence
is how frequently you want the job to run. With WordPress, by default, you only have three options: hourly, daily and twice daily. $hook
is the name of the task you want to run. We’ll come back to this in a bit.
Next, to finish this event off, we need to add an action.
add_action( 'my_task_hook', 'my_task_function' );
This is just like any other action, but there are a few key things to mention:
- The first parameter matches the
$hook
we’ve been using forwp_next_scheduled
andwp_schedule_event
, and - Like any other
add_action
, the second parameter is the name of the function that we actually want to run.
In other words, wp_schedule_event
is like a do_action
with a time and/or recurrence associated with it. At the given time, an event registered with wp_schedule_event
fires the same way it would if a do_action
was included in a function or template file. If you think about it like a do_action
, wp_cron
becomes much less abstract.
An important gotcha to be aware of when using wp_cron
is that, unlike its namesake, there is no guarantee that the cron task will actually execute at the scheduled time.
If you read between the lines–because it’s not explicitly stated there–the scheduled task only actually executes after the next scheduled time. And only then if the site gets hit. If no one (including admins) is hitting your site, the task never fires, no matter if it’s passed the scheduled time or not. wp_cron
, unlike normal cron
, only works insomuch as your site has a fairly regular stream of hits. Therefore, (much like transients) don’t expect your task to run at a specific time or interval. It can, and should, be used to run scheduled tasks, but the actual schedule itself could be extremely irregular based on the traffic of your site.
Using wp_cron
in the real world
Let’s look at a real-world example. I want to schedule a task, but I’m using a class and the example code isn’t showing me how to use the functions in a class. I also want to pull my schedule frequency from an option that’s set on a plugin options page. My option will save the frequency to my setting and in my __construct
function. I’ve got this to save the options so I can pull them out in my methods:
$this->options = get_option( 'wds_my_option_key' );
Now I’ll schedule some tasks.
public function do_hooks() { // get our options $schedule = $this->options['ping_frequency']; $hook = 'wds_my_task_hook'; // run wp_cron hooks if ( $schedule ) { // if the schedule doesn't match the option, delete the task so we can recreate it if ( $schedule !== wp_get_schedule( $hook ) ) { wp_clear_scheduled_hook( $hook ); } if ( ! wp_next_scheduled( $hook ) ) { wp_schedule_event( time(), $schedule, $hook ); } add_action( $hook, array( $this, 'wds_my_scheduled_function' ) ); } }
First, I get my option value for my ‘ping_frequency’ setting, which is where I’ve saved my scheduled frequency. Then, I’m saving the name of my hook to a variable, so I can just use $hook
in the functions later. Once that stuff is set up, I check to make sure that ping frequency has a value. If it doesn’t, I’m not going to schedule anything. Now I can check the saved frequency against the next scheduled event for my hook. This is comparing what I want my frequency to be versus what the frequency is actually set to, and the reason I’m doing this is to prevent scheduling multiple instances of the same hook at different times regardless of when you want it to fire.
This is because wp_cron
doesn’t care if you have multiple instances of the same task at different times. It’ll let you do that as much as you want. The updated one doesn’t overwrite the last one. So, comparing my setting against what’s actually scheduled will prevent me from adding multiple entries unnecessarily and only keeping the one that I set on my options page.
When that stuff is out of the way, I can actually schedule my event. Notice that nothing has changed, really, in my wp_schedule_event
call. It still looks the same as the example code on the Codex, I’m just using variables for the schedule and the hook. When I get to the add_action
call, I pass in my $hook
, and then an array with my class instance and the method I’m running for the action I want to execute, which is how you’d normally handle an action inside a class. Nothing drastically new.
Schedules!
Great, so now I can schedule a recurring task at one of three different schedules. That’s fantastic…except when I want to run something more frequently than once an hour, or less frequently than once a day, or anything in between once and twice a day. That leaves a lot of gaps there.
The way to solve this problem is with the cron_schedules
filter. You can find some example code for using this in the…wp_get_schedules documentation in the Codex. Not the first place I’d look. (And, for the record, that filter isn’t listed in the WP-Cron Functions category page in the Codex…probably because it’s not actually a wp_cron
function, but I digress.) Using the filter works like this:
add_filter( 'cron_schedules', 'cron_add_weekly' ); function cron_add_weekly( $schedules ) { // Adds once weekly to the existing schedules. $schedules['weekly'] = array( 'interval' => 604800, 'display' => __( 'Once Weekly' ) ); return $schedules; }
Pretty simple. You add an array key, and pass in an interval and a display name. Add this filter, and you can immediately start using this new schedule. For the record, passing a mathematical equation works fine for the duration, here, too. For example, instead of using 604800
, I could instead use 60 * 60 * 24 * 7
When there can be only one
What if I only want to run my task once? That’s handled with wp_schedule_single_event
. This might be useful if you wanted to run some function but you didn’t want to do it right away or during the normal execution of WordPress and you didn’t want to use a transient. This works almost 100% the same as wp_schedule_event
with one fairly significant difference.
With wp_schedule_event
, you typically use time()
to pass the current unix timestamp as the first parameter. This tells wp_cron
that you want to run the event right now, and then the next occurrence will happen at whatever recurrence rate you specified. If you’re only scheduling a single event, probably you don’t want it to happen right now–you probably want it to happen sometime in the future. So instead of passing time()
, you might do something like this:
wp_schedule_single_event( strtotime( time() . ' +2days' ), 'wds_my_event_hook' );
This time, I’m using strtotime
and adding two days to the current time, so it’ll happen in two days (or…you know, sometime after). strtotime (e.g. “string to time”) converts a string into a unix timestamp. It can take almost anything you throw at it, including the addition of “+ some amount of time” (like “+2 hours”). In this case, I’m throwing the current unix timestamp and +2 days to tell wp_schedule_single_event
to fire two days from now.
wp_cron
Performance vs. normal cron
Because wp_cron
is not a real cron, because the schedules and tasks are stored in the database, and because of the way that wp_cron
is executed, there is a potential performance hit on high-traffic sites. When the wp_cron
task executes, it sends an HTTP request to yourdomain.com/wp-cron.php. Ben Lobaugh has presented on wp_cron
at WordCamp Portland and WordCamp Orange County and has a solution to this problem.
In his presentation — which is in blog form on his site — he suggests disabling wp_cron
in your wp-config.php
and using your *nix server’s real cron system to ping the wp-cron.php
file at actual scheduled intervals. This ensures that wp-cron.php
is only hit once at the exact times that you want, and doesn’t need to query the database to figure out the schedule. You would still register your wp_cron
events normally, but the recurrence wouldn’t matter as much since you’re scheduling it through actual cron.
Winning
Now you can go forth and schedule all the things! As long as you remember not to expect those recurrences to come with any kind of real regularity, scheduling tasks with wp_cron
can be a useful tool when you want to run periodic checks. Currently I’m in the middle of a project where I’m using wp_cron to check the status of post translations from a third-party API.
If you’ve got any cool uses for wp_cron
functions or tips that I haven’t thought of, let us know in the comments!
Very nice article, thanks, Chris Reynolds for sharing this information.
Chris, it’s a greate tutorial!
You should test our “Cron Job Event Generator” at: http://generatewp.com/schedule_event/
That’s awesome! 🙂
Thanks for the article. For many users without PHP or Linux knowledge, using third party cron job service (like easycron.com) is a good alternative.
Hi Chris,
your post is really exhaustive. Only one point is still unclear for me.
Suppose I have a function that get executed once per hour.
What If I disable cron throught define(‘WP_CRON’,false) and I use standard *nix cron to run every 5 minutes? Are the planned function run more frequently than one per hour or the *nix cron frequency is irrilevant (so I only have to keep it as low as the lowest schedule)?
Thanks
If you disable WordPress cron, the internal WordPress cron schedule never runs. The tasks just sit in the queue indefinitely, even though they might have time-based frequencies associated with them (every 1 hour, every day, at this time, etc.). If you then add a *nix cron schedule to hit the wp-cron.php, that will trigger the WordPress cron schedule. Any WordPress cron tasks that are still in the queue (anything that wasn’t fired yet) will trigger normally.
TL;DR — Disabling WP_Cron and using *nix cron will still use the internal scheduling system, so things wouldn’t fire more frequently than they are scheduled.
Fantastic write up Chris!
You might be interested to know that it turns out that our favorite little event management plugin’s next major version uses wp_cron() for scheduling the processing of emails/messages. The idea was to avoid the bottleneck that can happen when several messages get processed and sent out all on the same request.
This also makes it possible to build features like sending automatic event reminders days/weeks before the event.
Bottlenecks are for real. I believe Mark Jaquith’s TLC Transients was built to resolve a similar bottlenecking issue with transients (I haven’t tried it personally): https://github.com/markjaquith/WP-TLC-Transients
Excellent post on WP Cron. Thank you Chris for sharing.