How WP-Cron actually works
WordPress keeps its scheduled events, the “cron” array, in one option in the
database: a list of hooks and the time each is due. Nothing watches that list. On
every page load WordPress reads it, and if the earliest event is due,
spawn_cron() sends a request from the site to itself, to
wp-cron.php. That request runs the due events.
Three details from core matter in practice. The request is a loopback:
wp_remote_post() to the site’s own wp-cron.php, with a
0.01 second timeout and no waiting for an answer, so the visitor is not held up.
A lock, the doing_cron transient, stops two requests starting the same
run within a minute. And on the front end the check happens on a page load, which
means a page that never loads WordPress never makes it.
Why it runs late, or not at all
- Nobody visits. No page load, no check. A quiet site, a staging copy or an intranet can sit on a due event for hours.
- A page cache or CDN answers first. A cached page is served without loading WordPress, so it checks nothing. A busy, well-cached site can miss as many checks as a quiet one.
- The loopback fails. The site has to be able to reach its own address. A firewall, basic authentication, a maintenance-mode plugin, a hosts file that points the domain at the wrong machine or a certificate error on the server itself can each stop that request, and WordPress does not say so.
- Long jobs. A run that takes minutes holds the lock and other events wait behind it.
Late is the usual result, not lost: the event stays in the list and runs on the next check that gets through. That is fine for a scheduled post and not fine for a backup you were counting on at 03:00.
ALTERNATE_WP_CRON is the fallback for hosts where the loopback cannot
work. Instead of a background request it redirects the visitor’s own request once,
with doing_wp_cron in the address, and runs the events from that. It
still needs a visit, it only acts on GET requests, and it puts a redirect in front
of that visit. It fixes the loopback, not the visitors, so I would only use it when
a server cron is not available.
Setting up a real server cron
Two steps, in this order. Order matters; the second is the one people forget.
1. Turn the page-load check off. In wp-config.php,
above the line that says That’s all, stop editing:
define( 'DISABLE_WP_CRON', true );
This only stops spawn_cron(). wp-cron.php still works when
something calls it, which is the point.
2. Run the due events from the server’s crontab. With WP-CLI:
*/15 * * * * cd /path/to/wordpress && wp cron event run --due-now --quiet --url=https://www.example.com
--due-now runs whatever is due and leaves the rest. --url
tells WordPress which site this is: on multisite it picks the site whose events
run, and on any install it is what settings that depend on the site’s address see,
since a command line has no request to take it from. Leave it off and a cron job
that works in a browser can quietly do nothing.
No WP-CLI? Call wp-cron.php over HTTP instead:
*/15 * * * * curl -fsS -o /dev/null "https://www.example.com/wp-cron.php?doing_wp_cron"
That is a real request through the web server, so it meets the same firewall and cache rules as a visitor. WP-CLI does not. Where both are possible, use WP-CLI.
Do not set DISABLE_WP_CRON without the crontab line.
With the constant set and nothing calling cron, nothing runs your scheduled events
at all: no scheduled posts, no backups, no cleanup, no update checks, and no error
to tell you.
Which interval
Every 5 to 15 minutes is enough for nearly every site: the cron only has to be often enough that a due event waits a few minutes, not hours. Every minute works but means loading WordPress sixty times an hour for nothing on a typical site, and shared hosts will count it against your processes. If your events need minute precision, you have a queue problem, not a cron problem.
Where hosts put it
Each hosting panel calls it something different. I am describing, not showing screenshots of anyone’s account.
- cPanel: Cron Jobs, under Advanced. Pick the interval, paste the command; the line above minus its schedule goes in the Command box.
- Plesk: Scheduled Tasks, on the subscription’s tools. Choose Run a command (or Fetch a URL for the curl form) and set the schedule.
- DreamHost: Cron Jobs in the panel, under More. Pick the user the site runs as, and enter the command and schedule.
- Your own server:
crontab -eas the user that owns the site’s files, not as root. A cron that creates files as the wrong owner is a second problem.
Some hosts do not offer WP-CLI in the cron environment, or run it with a different
PHP than the site. If wp works in your shell and fails in the cron job,
give the cron line the full path to it, and compare which php each
one finds.
Checking it works
List what is scheduled and when it is due:
wp cron event list --url=https://www.example.com
A healthy site has no event whose next run is more than one cron interval in the past. Events a day overdue mean nothing is calling cron. And run it once by hand before you trust the crontab; whatever error the cron job would have swallowed shows up in front of you.
This part stands on its own. It is the same for scheduled posts, WooCommerce actions, update checks, and anything else registered with WP-Cron.
Scheduled backups on top of it
Freshet Backup
schedules its run as an ordinary WP-Cron event, freshet_backup_run, so
it is exactly as punctual as the cron under it. Behind that sits a catch-up: when
a whole interval has gone by since a backup was due, the next time an administrator
opens wp-admin the backup starts after the page has been sent, at most once per
interval. If nothing has succeeded in twice the interval, a notice appears on every
wp-admin page.
The catch-up has a limit I would rather say plainly. It needs an administrator to open wp-admin. On a site nobody logs in to, it does nothing, and the backup is as late as the cron. It is a net, not a schedule; a server cron is the schedule.
On a multisite network backups run from the main site only, and the catch-up and the settings screen are for a network administrator, not a site administrator. The cron line below belongs to the main site’s address.
To start a backup now from a shell, run the event:
wp cron event run freshet_backup_run --url=https://www.example.com
Keep the --url there too. The Dropbox connection only applies on the
site’s own addresses, and a command without one names none.
To see which way your backups are being started, open Settings → Freshet Backup and
read the end of the Last run row. Started on schedule
means cron is doing its job. Started by the catch-up means WP-Cron did not
run it and a visit to wp-admin did, which is the signal to set up the crontab line
above. wp cron event list shows the next backup and whether it is
overdue.
Since version 1.1.2 the same screen has a Server cron block, so
you do not have to assemble the lines from this article. It prints the crontab
line for your site, with its own path and address filled in, in the WP-CLI form
and the curl form, plus the DISABLE_WP_CRON line for wp-config.php,
every 15 minutes, and the warning not to add the constant without the crontab.
Freshet Backup changes neither file; you copy the lines yourself. The block also
tells you which way the schedule is driven: Run on page loads when
DISABLE_WP_CRON is not set, Run from the server when it is
set and the last backup started on schedule, and Run from the server, but
nothing is calling it when it is set and the last backup still needed the
catch-up. That last one means the crontab line is missing or not working, and the
block opens itself to show it.