Crontab Guide

Editing, listing and removing cron jobs, plus the crontab-only syntax the builder can't show you

A cron job has two parts: the cron expression and the crontab. The crontab is the file that pairs each schedule with a command. It has its own syntax and its own environment, plus several rules that can stop a job from running without any warning. This guide covers editing and listing crontabs, the difference between user and system crontabs, the environment variables that matter, and the escaping rules that most people trip over at least once.

Anatomy of a crontab line

A user crontab line is a cron expression followed by the command to run. Everything after the fifth field is the command, spaces included, and cron does not need quotes around it.

# ┌───────────── minute (0-59)
# │ ┌─────────── hour (0-23)
# │ │ ┌───────── day of month (1-31)
# │ │ │ ┌─────── month (1-12)
# │ │ │ │ ┌───── day of week (0-6, Sunday = 0)
# │ │ │ │ │
  30 2 * * *  /usr/local/bin/backup.sh --full

Blank lines are ignored and lines starting with # are comments. Comments must be on their own line: cron does not strip a trailing # from a command line, so 0 3 * * * /bin/job.sh # nightly passes # nightly to the shell rather than discarding it. That is harmless in this example, but it can cause problems when the comment contains a character the shell acts on.

Use the builder to compose the schedule and the explainer to verify a line you inherited; this guide covers everything to the right of the fifth field.

Editing and listing your crontab

Never edit the spool file directly. The crontab command validates syntax and signals the daemon to reload; hand-editing /var/spool/cron/ does neither.

The crontab command
CommandWhat it does
crontab -eOpen your crontab in an editor. On save, cron parses it and rejects the whole file if any line is malformed.
crontab -lPrint your crontab to standard output.
crontab -rDelete your crontab. There is no confirmation and no undo.
crontab -i -rDelete with a confirmation prompt. It is worth aliasing crontab -r to this.
crontab -u deploy -lList another user's crontab. Requires root.
crontab jobs.txtReplace your entire crontab with the contents of a file. This lets you keep cron jobs in version control.
crontab -r is one keystroke from crontab -e

Deleting a crontab by typo happens often enough that it turns up regularly in incident reports. Keep the authoritative copy in version control and deploy it with crontab jobs.txt, so a stray -r costs you a redeploy rather than a reconstruction from memory.

Choosing the editor

crontab -e uses $VISUAL, then $EDITOR, then a system default that is often vi. To use a different editor, set it explicitly:

EDITOR=nano crontab -e

User crontabs vs system crontabs

There are two formats, and they differ by one extra field. Mixing them up is a frequent reason for "my job never runs".

Where cron jobs live
LocationFormatNotes
crontab -e5 fields + commandPer-user. Runs as the owning user. Stored under /var/spool/cron/.
/etc/crontab5 fields + user + commandSystem-wide. The sixth field names the user to run as.
/etc/cron.d/*5 fields + user + commandDrop-in files, same format as /etc/crontab. The usual place for package- and config-management-owned jobs.
/etc/cron.daily/Executable scripts, no scheduleAlso hourly, weekly, monthly. Run by run-parts at a time the distribution chooses.

A system crontab line looks like this. Note the root before the command:

30 2 * * * root /usr/local/bin/backup.sh --full

Put that same line in a user crontab and cron tries to execute a program called root. Put a user crontab line in /etc/cron.d/ and cron treats the first word of your command as the username, fails to resolve it, and logs an error that you will only see if you check the logs.

Files in /etc/cron.d have naming rules

With run-parts semantics, filenames containing a dot are skipped entirely. Cron silently ignores a drop-in named backup.cron or my-job.sh, so name it backup instead. The same rule applies to scripts in /etc/cron.daily/.

The environment: PATH, SHELL and MAILTO

The environment causes more "it works in my terminal but not in cron" problems than anything else. Cron does not read .bashrc, .bash_profile, .profile or any other login file, so none of your aliases, exported variables, version manager shims or PATH additions are present.

A typical cron environment looks roughly like this:

SHELL=/bin/sh
PATH=/usr/bin:/bin
HOME=/home/youruser
LOGNAME=youruser

Two consequences follow. First, /usr/local/bin is usually not on the path, so anything you installed yourself will not be found by name. Second, the shell is /bin/sh, not bash, so bashisms such as [[ ]], &> and arrays may fail depending on what sh points at.

You can set variables at the top of a crontab. Assignments apply to every line below them:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@example.com

0 3 * * * /usr/local/bin/nightly.sh
Crontab environment variables
VariableEffect
SHELLWhich shell runs each command. Defaults to /bin/sh.
PATHCommand lookup path. It is deliberately minimal by default.
MAILTOAddress that receives any output a job produces. MAILTO="" disables mail entirely.
CRON_TZTime zone for the schedules below it, in Vixie-derived crons. See the time zones guide.
HOMEWorking directory for the job. Defaults to the user's home.

Variable assignment in a crontab is literal and does not follow shell syntax. PATH=$PATH:/opt/bin does not expand. Cron stores the string $PATH:/opt/bin verbatim. Write the full path out, or set it inside the script instead.

The most reliable pattern

Keep the crontab line trivial and put all the logic in a script: 0 3 * * * /usr/local/bin/nightly.sh. Inside the script, use a shebang, set the variables you need, and use absolute paths. One-liners with pipes and redirects in a crontab tend to collect escaping bugs. A script can also be run by hand for testing, which a crontab line cannot.

Escaping: the percent sign rule

Inside a crontab, an unescaped % has a special meaning. Cron removes the first one and everything after it from the command and feeds that text to the command on standard input, turning each later % into a newline. The feature exists for supplying input to a program, but it catches out anyone who uses date in a command.

# Wrong: everything from %Y onward becomes stdin
0 1 * * * /bin/tar czf /backup/$(date +%Y%m%d).tgz /srv

# Right: escape each percent
0 1 * * * /bin/tar czf /backup/$(date +\%Y\%m\%d).tgz /srv

Because this rule applies only to crontabs and not to scripts, the same command that fails as a crontab line works unchanged inside a shell script. That is another reason to keep the crontab line trivial.

The missing final newline

Historically, a crontab file whose last line had no trailing newline would have that last job silently ignored. Modern crontab -e fixes this for you, but a crontab installed from a file (crontab jobs.txt) or written into /etc/cron.d/ by a deploy script can still hit it. If your last job is the only one not running, check for the newline first.

Capturing output and exit codes

By default, anything a cron job writes to stdout or stderr is mailed to the job's owner. On a machine with no mail transport configured, which describes most machines now, that output goes nowhere. A job can fail every night for months without anyone noticing.

Redirect output explicitly:

# Append both streams to a log
0 3 * * * /usr/local/bin/nightly.sh >> /var/log/nightly.log 2>&1

# Log errors only; discard normal output
0 3 * * * /usr/local/bin/nightly.sh > /dev/null

# Timestamp each line
0 3 * * * /usr/local/bin/nightly.sh 2>&1 | /usr/bin/ts >> /var/log/nightly.log

A common anti-pattern is > /dev/null 2>&1, which discards errors along with normal output, so a broken job stays broken without anyone knowing. If you want no output on success and a report on failure, have the script exit non-zero and write to stderr only when something goes wrong. Cron will then mail you (if mail works), or your monitoring will notice the non-zero exit. A dead-man's-switch check-in, described in the best practices guide, is a better option still.

Permissions and cron.allow

Whether a user may install a crontab at all is controlled by two files:

A user who is refused sees You (name) are not allowed to use this program from crontab -e. These files govern installing crontabs, not running jobs. A job that is already installed keeps running even if the user is later added to cron.deny.

Beyond that, ordinary Unix permissions apply. The script must be executable (chmod +x), readable by the user cron runs it as, and every directory in its path must be traversable by that user. A job that runs as www-data cannot read a script in a home directory set to mode 700.

A checklist before you save

  1. Does the schedule mean what you think? Paste it into the Explainer and read the next five run times, not the syntax.
  2. Are both day-of-month and day-of-week restricted? If so, cron applies OR logic and the job runs more often than you expect.
  3. Is every path absolute, including the interpreter, the script, the data files and the log?
  4. Is every % escaped as \%?
  5. Is output redirected to a place you check regularly?
  6. Does the script run correctly under a minimal environment? Test with env -i /bin/sh -c '/usr/local/bin/yourjob.sh'.
  7. Is the file version-controlled and deployed with crontab jobs.txt, rather than typed into an editor on a production box?
  8. Can the job overlap with itself if it runs long? If so, wrap it in flock.

If a job still does not fire after all of that, work through the troubleshooting guide, which starts at the daemon and the logs rather than the crontab.