Crontab Guide

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

A cron expression is only half of a cron job. The other half is the crontab — the file that pairs each schedule with a command, and that carries its own syntax, its own environment, and a handful of rules that will happily eat your job without telling you. This guide covers editing and listing crontabs, the difference between user and system crontabs, the environment variables that matter, and the escaping rules that catch everyone 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 — cron does not need quoting 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. Harmless in that example, less harmless 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. 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 is how you version-control cron jobs.
crontab -r is one keystroke from crontab -e

Deleting a crontab by typo is common enough to be a genre of incident report. 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. Set it explicitly if that surprises you:

EDITOR=nano crontab -e

User crontabs vs system crontabs

There are two formats, and the difference is one extra field. Mixing them up is a frequent cause of "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 you will only find if you go looking.

Files in /etc/cron.d have naming rules

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

The environment: PATH, SHELL and MAILTO

This is the single biggest source of "it works in my terminal but not in cron". Cron does not read .bashrc, .bash_profile, .profile or any other login file. Your aliases, your exported variables, your version manager shims and your PATH additions are all absent.

A typical cron environment is roughly:

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

Note that variable assignment in a crontab is literal, not 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 robust 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. A crontab full of one-liners with pipes and redirects is a crontab full of escaping bugs waiting to happen — and a script can be run by hand for testing, which a crontab line cannot.

Escaping: the percent sign rule

Inside a crontab, an unescaped % is special. The first one and everything after it is removed from the command and fed to the command on standard input, with each subsequent % becoming a newline. This is a feature for supplying input to a program, and a trap for anyone using date.

# 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 — another argument for keeping 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 is most machines now — that output goes nowhere. A job can fail every night for months without producing a single visible signal.

Redirect 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

The common anti-pattern is > /dev/null 2>&1, which discards errors as well as output and guarantees that a broken job stays broken quietly. If you want silence on success and noise on failure, let 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. Better still, use a dead-man's-switch check-in as described in the best practices guide.

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. Note that these files govern installing crontabs, not running jobs — a job 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 — the interpreter, the script, the data files, the log?
  4. Is every % escaped as \%?
  5. Is output redirected somewhere you will actually look?
  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.