Cron is the scheduler built into Unix-like systems. You give it a command and a time pattern, and it runs the command whenever the pattern matches. Backups, cleanup, report emails, certificate renewals and cache warming all depend on it, which makes it one of the least glamorous and most load-bearing parts of a server.
It is also the source of a particular kind of misery: the job that silently stopped three weeks ago, the one that runs at the wrong hour, the one that works in your terminal and fails under cron. WordPress adds its own twist with a scheduler that shares the name and almost nothing else. This piece covers how cron reads a schedule, what goes wrong, how WP-Cron differs, and how to notice when a job has stopped.
Reading a schedule
A cron line has five time fields followed by the command: minute, hour, day of month, month, day of week. An asterisk means every value. A comma makes a list, a hyphen a range, and a slash a step.
30 2 * * * means 02:30 every day. */15 * * * * means every fifteen minutes. 0 9 * * 1 means Mondays at nine. 0 8-18 * * 1-5 means on the hour from eight to six on weekdays. Most cron implementations also accept shortcuts such as @daily, @hourly and @reboot, though a hosting control panel may offer only the five-field form.
One oddity catches people out. If you set both day of month and day of week, classic cron runs the job when either matches, not both. 0 9 1 * 1 fires on the first of the month and on every Monday. If you want "the first Monday", you have to test the date inside the script.
The cron helper on this site turns a plain description into a line and back, which is a good way to check a pattern before it goes live.
Time zones and the clock changing
Servers have a time zone, very often UTC, and cron uses it. A job you expect at nine in the morning your time may run at a different hour, and for some of the year at a different hour again. In Bulgaria, for example, nine local time is 06:00 or 07:00 UTC depending on summer time, so a fixed UTC schedule drifts against the local clock twice a year.
If the server runs on local time, the clock changes cause their own trouble. When clocks go forward, jobs scheduled inside the skipped hour may not run at all. When they go back, jobs scheduled inside the repeated hour can run twice. Most cron implementations try to handle this gracefully for jobs at fixed times, but wildcard jobs and long-running ones still surprise people. The simple defence is to schedule important jobs away from the small hours of the change night, and to keep servers on UTC unless there is a strong reason not to.
To see what the server believes the time is, run date and cat /etc/timezone (or timedatectl on systemd machines). On shared hosting you often cannot change the zone, so convert in your head and write the conversion into a comment in the crontab.
Why it works in your terminal and fails under cron
Cron runs with a minimal environment. There is no login shell, so no profile scripts, a short PATH (often just /usr/bin:/bin) and none of the variables you set by hand. A command that works when you type it can fail under cron because it finds a different PHP, cannot find wp at all, or lacks a variable the script assumes.
# Fragile
*/10 * * * * php artisan schedule:run
# Better: full paths, explicit directory, output kept
*/10 * * * * cd /home/example/app && /usr/bin/php artisan schedule:run >> /home/example/logs/cron.log 2>&1
Use full paths to programs (which php tells you where yours lives), change into the right directory first, and redirect both output streams to a log file. Without the redirect, cron tries to email the output to the account owner or the MAILTO address, and on many servers that mailbox is one nobody reads. A job that has been failing noisily into an unread mailbox for a year is a classic find.
Percent signs are another trap. Inside a crontab line, an unescaped % means a newline, so a date +%F in a command must be written date +\%F. If a backup job creates files with odd names or never runs, look for this first.
Make jobs safe to run twice
Eventually a job will run twice, or two copies will overlap because the first was still working when the next was due. A report that emails customers twice is embarrassing; a cleanup that deletes the wrong directory when two copies collide is expensive. Design for it.
The cheap fix for overlap is a lock. On Linux, flock does it in one line, and the second copy exits immediately if the first is still running:
*/5 * * * * /usr/bin/flock -n /tmp/import.lock /home/example/bin/import.sh
Beyond that, write jobs so that repeating them changes nothing: process rows marked as pending and then mark them done, write output to a temporary name and rename it when complete, check whether today's backup file already exists before making another. Keep an eye on run time as well. A job scheduled every five minutes that now takes seven is a slow-motion pile-up.
A worked example: a small shop exports new orders to a courier every ten minutes. The script reads rows where exported = 0, sends them, and sets exported = 1 only after the courier confirms. If cron fires the job twice, or the server restarts halfway, the next run picks up whatever is still marked zero. Nothing is sent twice, nothing is lost, and nobody has to clean up by hand. The same shape works for sending newsletters, resizing images and syncing stock.
WP-Cron is not cron
WordPress has its own scheduler, and it works on a completely different principle. It is triggered by visits. When someone loads a page, WordPress checks whether any scheduled task is due, and if so it fires a background request to wp-cron.php to run it. Nothing is watching the clock; the traffic is the clock.
On a busy site this means the check happens on every request and adds load, and on a slow server the spawned requests can stack up. On a quiet site it means scheduled posts are missed ("Missed schedule" in the dashboard) and backups run late, because nobody visited. It also fails outright when the site is behind HTTP authentication, or a firewall blocks the server from calling itself, or a caching layer swallows the background request.
The common fix has two halves. First disable the built-in trigger in wp-config.php:
define('DISABLE_WP_CRON', true);
Then have a real cron job call WordPress every five or ten minutes:
*/5 * * * * /usr/bin/php -q /home/example/public_html/wp-cron.php >/dev/null 2>&1
# or, where the command line tool is installed:
*/5 * * * * cd /home/example/public_html && /usr/local/bin/wp cron event run --due-now >/dev/null 2>&1
Do both, not one. Disabling the trigger without adding the cron job means nothing scheduled will ever run again, and that mistake is more common than you would think.
Shared hosting, VPS and managed plans
On shared hosting, cron is usually a form in the control panel. It can be limited: some hosts set a minimum interval (once every five or fifteen minutes), a cap on run time, or a cap on the number of jobs. Read the limits before designing around one-minute jobs. Output paths are within your account, and the PHP binary is often a specific version path, so check which one the panel offers.
On a VPS or dedicated server you edit the crontab yourself with crontab -e, list it with crontab -l, and can use system-wide files in /etc/cron.d/ or systemd timers, which add logging and dependency handling. You are also responsible for the time zone, the mail settings and the log rotation. Managed WordPress hosts typically handle the real-cron replacement for you; the setting is usually labelled something like "server-side scheduler", and it is worth confirming it is on.
Monitor the quiet failures
A job that stops running produces no error. Nothing crashes, nobody is told, and the first sign is the day you need the backup. The only reliable defence is to flip the monitoring around: do not wait for an error, expect a signal. Use a heartbeat service that expects a ping every day and alerts you if it does not arrive. It takes minutes to set up.
30 2 * * * /home/example/bin/backup.sh && curl -fsS -m 10 --retry 3 https://ping.example.com/abc123 >/dev/null
The ping follows &&, so it fires only when the backup exits successfully. If the script fails, or cron stops, or the server is down, the ping never arrives and the alert goes out. Choose a grace period that matches the job: a nightly backup might alert after 26 hours. For jobs that run every few minutes, check the log's modification time instead. For the surrounding reliability questions, the uptime calculator shows how little downtime a monthly target actually allows.
Try it on your own site
- List what is scheduled:
crontab -l, or open the panel's cron page. Note the time zone withdate. - Run the exact command by hand with a stripped environment:
env -i /bin/sh -c 'cd /home/example/app && /usr/bin/php job.php'. If it fails here, it fails under cron. - Check the log:
grep CRON /var/log/syslog | tailon Debian-style systems, or/var/log/cronon Red Hat-style ones, shows each start. On shared hosting, read your own output file. - For WordPress, run
wp cron event listand look at the "next run" column. Entries with a time far in the past mean the scheduler is not firing. - Look at the troubleshooting guide if the job runs but its effects do not show.
Things people ask
How often should wp-cron.php be called?
Every five minutes suits most sites; every minute only if you publish on tight schedules or run a shop with timed jobs. Ten is fine for a quiet blog.
My scheduled post missed its time. Is that cron?
Almost always, yes: no visitor arrived in the right window, or the background request failed. Moving to a real cron job normally ends it.
Is it safe to leave the cron job output going to /dev/null?
For frequent, trivial jobs, yes. For anything that matters, write to a log and rotate it, because the first time something breaks you will want the error message.
Can two cron jobs run at the same moment?
Yes, and they usually do at midnight and on the hour, which is why heavy jobs belong at an odd minute such as 03:17 rather than 03:00.