DEV Community

bigdomain host
bigdomain host

Posted on Originally published at bigdomainhost.com

Cron jobs, and the four reasons yours isn't running

Disclosure: I run Bigdomainhost, an Indian hosting company. This is adapted from a guide on our site.

Cron takes a minute to understand and, sometimes, a week to debug. The idea is trivial: a table of commands, each with a schedule, and a daemon that runs them on time. It has been part of Unix since the 1970s.

The trouble is that a cron job which looks right will often just not run, and cron won't tell you. When that happens it's almost always one of four things. Syntax first, then those four.

The five fields

┌───────────── minute        (0 - 59)
│ ┌─────────── hour          (0 - 23)
│ │ ┌───────── day of month  (1 - 31)
│ │ │ ┌─────── month         (1 - 12)
│ │ │ │ ┌───── day of week   (0 - 6, Sunday = 0)
│ │ │ │ │
* * * * *  /usr/bin/php /home/user/script.php
Enter fullscreen mode Exit fullscreen mode

There are only four operators:

  • * means every value. * * * * * is every minute.
  • , is a list. 0 9,17 * * * is 9am and 5pm.
  • - is a range. 0 9-17 * * * is hourly from 9am to 5pm.
  • / is a step. */15 * * * * is every fifteen minutes.

That's the whole language. Anything that looks more complicated is a combination of these.

Schedules you'll actually use

Expression Runs
*/5 * * * * Every five minutes
*/15 * * * * Every fifteen minutes (the usual WP-Cron replacement)
0 * * * * Every hour, on the hour
30 2 * * * 2:30am daily, the classic backup slot
0 3 * * 0 3am every Sunday
0 0 1 * * Midnight on the first of every month
0 9 * * 1-5 9am on weekdays only

One thing people forget: cron runs in the server's timezone. On our servers that's IST, so 30 2 * * * means 2:30am in India. On a cloud VM somewhere else it might be UTC, and your "2:30am backup" quietly runs at 8am IST in the middle of business hours. Run date on the box before you assume anything.

The smallest interval standard cron gives you is one minute. If you need something finer, you need a long-running process or a loop with sleep, not cron. On shared hosting the practical minimum is often five or fifteen minutes, to protect everyone else on the server.

Setting it up

In cPanel

  1. cPanel → Cron Jobs.
  2. Put a real address in Cron Email. Cron mails you anything the command prints. This is how you find out a job has been failing for three weeks.
  3. Pick a Common Setting, or type the five fields yourself.
  4. Enter the command with absolute paths:
/usr/local/bin/php /home/username/public_html/cron.php
Enter fullscreen mode Exit fullscreen mode
  1. Add it, wait for the first run, and check the mail.

Once you've seen it succeed and you're tired of the emails, append >/dev/null 2>&1. Not before. Silencing a job you haven't watched work is how jobs die unnoticed.

On a VPS

# edit your own crontab
crontab -e

# list what is scheduled
crontab -l

# edit another user's crontab
sudo crontab -u www-data -e
Enter fullscreen mode Exit fullscreen mode

System-wide jobs go in /etc/cron.d/, and those files take an extra field: the user to run as, between the schedule and the command.

# /etc/cron.d/backup
30 2 * * * root /usr/local/bin/backup.sh
Enter fullscreen mode Exit fullscreen mode

Copy a line from /etc/cron.d/ into crontab -e (or the other way round) and the user field gets treated as the command, or the command loses its first word. This catches people out constantly.

The four reasons it doesn't run

1. Relative paths

Cron doesn't start in your home directory and doesn't have your shell's PATH. So:

php script.php                              # fails
/usr/local/bin/php /home/user/script.php    # works
Enter fullscreen mode Exit fullscreen mode

Find the real binary with which php and paste exactly what it prints. Same for mysqldump, node, python3, anything.

2. A missing environment

Your interactive shell loads .bashrc. Cron doesn't. If your script depends on an environment variable, it needs to be set in the crontab itself:

PATH=/usr/local/bin:/usr/bin:/bin
30 2 * * * /home/user/backup.sh
Enter fullscreen mode Exit fullscreen mode

3. The script isn't executable

chmod +x /home/user/backup.sh
Enter fullscreen mode Exit fullscreen mode

And make sure the first line is a shebang, #!/bin/bash. Without it, cron may not know how to run the file.

4. An unescaped percent sign

This is the one nobody guesses. In a crontab, % means newline. Everything after the first % gets passed as stdin, not as part of the command. So this:

30 2 * * * tar czf /backups/site-$(date +%Y-%m-%d).tar.gz /home/user/public_html
Enter fullscreen mode Exit fullscreen mode

gets cut off at date +, and you get a broken command with no obvious error. Escape every one:

30 2 * * * tar czf /backups/site-$(date +\%Y-\%m-\%d).tar.gz /home/user/public_html
Enter fullscreen mode Exit fullscreen mode

Or, honestly, put the logic in a shell script and call the script from cron. Then the % problem goes away entirely and you can test the script by hand.

WordPress: replace WP-Cron

WordPress has its own scheduler called WP-Cron, and it isn't cron. It fires when someone loads a page. On a quiet site that means scheduled posts don't publish and backup plugins don't run. On a busy site it fires on every page load, which is its own problem.

You fix both in five minutes. In wp-config.php:

define('DISABLE_WP_CRON', true);
Enter fullscreen mode Exit fullscreen mode

Then add a real cron job every fifteen minutes:

*/15 * * * * /usr/local/bin/php /home/user/public_html/wp-cron.php >/dev/null 2>&1
Enter fullscreen mode Exit fullscreen mode

For five minutes of work, few WordPress changes pay off more. We go into it, and the rest of the WordPress tuning list, in our WordPress speed guide.

Debugging a job that's gone quiet

In the order I'd actually do it:

  1. Run the exact command by hand. Copy it out of the crontab, paste it into a shell. If it fails interactively, cron was never the problem.
  2. Check the cron log. /var/log/syslog on Debian and Ubuntu, /var/log/cron on AlmaLinux and Rocky. If there's no line for your job at the expected time, the schedule or the crontab file is wrong. If there is a line, the command is wrong.
  3. Capture the output. Replace >/dev/null 2>&1 with >> /home/user/cron.log 2>&1 and read the file after the next run.
  4. Test with * * * * *. You find out in a minute instead of tomorrow. Then set the real schedule back.

A quick way to check the log on Ubuntu:

grep CRON /var/log/syslog | tail -20

# if there's no syslog file, ask journald instead
journalctl -u cron --since today
Enter fullscreen mode Exit fullscreen mode

On AlmaLinux and Rocky the unit is crond, not cron.

If you see the job firing there but nothing happens, go back to the four reasons above. Paths and % are the two I'd check first.

Cron is available on every plan we sell, from shared cPanel hosting upwards, and on a VPS you get the full crontab and /etc/cron.d/. The original guide, with a short FAQ, is on our site.


Posted by the Bigdomainhost team. The original version of this guide is on our site.

Top comments (0)