Employee Post

Using wp_cron (or “How to cron a cron that’s not really a cron”)

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.

wp-cron-sunrise

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.

windows-schedule-task

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)

prince-eyelook

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:

wp_cron

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.

wp_cron-return

Wait, so there’s no return values and…the examples don’t even use wp_cron???

what-does-it-mean

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.

wp_next_scheduled

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.

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:

  1. The first parameter matches the $hook we’ve been using for wp_next_scheduled and wp_schedule_event, and
  2. 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.

wp_schedule_event-desc

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!

smokin-mask

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

highlander-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

designer-dollar-signs

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!

8 thoughts on “Using wp_cron (or “How to cron a cron that’s not really a cron”)

  1. 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.

  2. 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

    1. 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.

  3. 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.

Have a comment?

Your email address will not be published. Required fields are marked *

accessibilityadminaggregationanchorbackupsbookmarksbuddypresscachingcalendarcaret-downcartunifiedcrediblecustommigrationdesigndevecomfriendsgoodgroupsgrowthhostingideasinternationalizationiphoneloyaltymailhealthmessagingArtboard 1migrationsmultiple-sourcesmultisitenotificationsperformancephoneprofilesresearcharrowscalablescrapingsecuresharearrowarrowsourcestreamsupportunifiedupdatesvaultwebsitewordpress