Sign in
← Linux: administration
10 min · easy · Lesson 4 of 10

Schedules: cron and crontab

How to run jobs on a schedule: the five-field crontab format, common mistakes, and where the output goes.

A backup at night, cleaning up temp files once an hour, a report every Monday. Jobs like these should run on their own, on a schedule, with nobody at the terminal. The classic tool for this is cron: a daemon (a background process) that reads schedules and runs commands at the right minute.

cron is on almost every Linux server. It's simple, and that's exactly why it's easy to get wrong in a way that leaves a job silently broken for years. This lesson is about the schedule format, and about why a script that works in the terminal fails under cron.

Where to practice

Every example below assumes a ubuntu:24.04 container with the cron package. cron isn't installed in a plain Ubuntu image, so this lesson's labs build their own image and run the daemon as the container's main process. On your own Ubuntu or Debian machine, it's most likely already there: crontab -l will answer either with a list of jobs or with no crontab for <name>.

macOS has crontab too, but Apple recommends launchd instead, and the details differ. Practice in the labs or in WSL2.

crontab: a user's schedule

Every user has their own schedule, called a crontab. Jobs in it run with that user's permissions. You work with it through the command of the same name:

crontab -l      # show your jobs
crontab -e      # open your crontab in an editor
crontab -r      # delete your whole crontab

crontab -e opens the editor from the EDITOR variable (in the labs that's nano). If there's no crontab yet, you'll see a header of comments. cron skips lines starting with #; add your own jobs at the end. After you save, the command answers:

crontab: installing new crontab

No need to restart cron: the daemon notices changes on its own and picks them up by the next minute.

Be careful with crontab -r. The letter r sits next to e on the keyboard, and the command wipes the whole schedule without asking:

crontab -r
crontab -l
no crontab for app

Root can work with someone else's schedule via -u: crontab -u app -l.

Format: five fields

A crontab line is five time fields and a command. The fields are separated by spaces:

┌── minute (0-59)
│ ┌── hour (0-23)
│ │ ┌── day of month (1-31)
│ │ │ ┌── month (1-12 or jan, feb, ...)
│ │ │ │ ┌── day of week (0-7, 0 and 7 = Sunday, or sun, mon, ...)
│ │ │ │ │
* * * * *  command

Order matters: minute first, then hour. The line 0 3 * * * means 03:00, while 3 0 * * * means 00:03.

What you can write in a field:

Examples:

0 3 * * *        /usr/local/bin/backup.sh       # every day at 03:00
*/15 * * * *     /usr/local/bin/check.sh        # every 15 minutes
0 9 * * 1        /usr/local/bin/report.sh       # Mondays at 09:00
0 0 1 * *        /usr/local/bin/monthly.sh      # midnight on the 1st
0 9-17 * * *     /usr/local/bin/ping.sh         # every hour from 09:00 to 17:00

One subtlety about days. If both the day of month and the day of week are set (neither is *), cron runs the job when at least one of them matches. The line 0 0 1 * 1 fires on the 1st of every month, and also on every Monday. If you only want specific days of the week, leave the day of month as *.

cron takes its time from the server's time zone. Servers and containers often live in UTC, so 0 3 * * * is 06:00 at UTC+3. Check the server's time with date:

date
Fri Oct  9 09:51:48 UTC 2026

How cron checks a line

It doesn't, and that's an unpleasant surprise. crontab on Ubuntu 24.04 saves a line with an error without a single word of complaint: both 61 * * * * and a line with only four fields. For example:

crontab -l
* * * * * date >> /tmp/good.txt
* * * * echo four >> /tmp/four.txt

A minute later, /tmp/good.txt showed up, but /tmp/four.txt didn't. The second line has only four fields; cron took the word echo for the day of week and silently skipped the whole line. The neighboring lines still work. So after any edit, always read back crontab -l and check that the job actually ran.

There's one more character with a special meaning: %. In a crontab line it means a newline, and everything after the first % goes to the command's standard input instead of the command line. The job date +%F > /tmp/d.txt won't even create the file under cron. In crontab, % is escaped with a backslash (date +\%F), or the command is moved into a script: inside a script, % is an ordinary character.

The job's environment

cron doesn't run a job from your terminal; it runs it itself, through /bin/sh. The job gets a stripped-down environment. Here's what user app's job sees in the lab (the line * * * * * env > /tmp/env.txt):

HOME=/home/app
LOGNAME=app
PATH=/usr/bin:/bin
LANG=C.UTF-8
SHELL=/bin/sh
PWD=/home/app

Three rules follow from this.

  1. PATH is short. It has no /usr/local/bin, /usr/sbin, or anything you added. A command the terminal finds gives not found under cron. Write full paths (/usr/local/bin/tool, not tool), or set PATH at the top of the crontab.
  2. The profile isn't read. The job won't see ~/.bashrc, ~/.profile, or anything you exported there. Set the variables you need in the crontab or in the script itself.
  3. The current directory is the home directory. The job starts in $HOME, not wherever the script lives. Relative paths (./data/file.csv) point somewhere else.

Variables in a crontab are set as a NAME=value line, before the jobs that need them:

PATH=/opt/tools/bin:/usr/bin:/bin
DATA_DIR=/srv/data
0 * * * * /opt/tools/bin/sync.sh >> /var/log/sync.log 2>&1

The value is taken literally; there's no substitution. PATH=$PATH:/opt/tools/bin turns into the literal string $PATH:/opt/tools/bin, and the job loses even /usr/bin. After a line like that, the job fails on the very first ordinary command with not found.

Where the output goes

Whatever a job prints to stdout and stderr, cron tries to mail to the crontab's owner. There's usually no mail system (MTA) on the server, and the output is discarded. On Ubuntu, cron logs a message to the system journal at that point, No MTA installed, discarding output, but the system journal isn't set up everywhere either. In the end, the job's errors are nowhere to be seen.

So the output gets redirected to a log file:

0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

>> file appends stdout to the end of the log, 2>&1 sends stderr wherever stdout is currently pointing. Order matters (covered in detail in the lesson on pipes and redirects). 2>&1 >> file sends stderr to the old stdout, meaning the mail that disappears, and only the normal output lands in the log.

The job's user needs permission to write to the log file. The app job won't be able to append to /var/log/backup.log if root owns the file. The redirection error goes nowhere too.

System schedules

Besides users' crontabs there are system ones: the file /etc/crontab and files in /etc/cron.d/. You edit them with an ordinary editor, not the crontab command, and they have one more field between the time and the command: the username.

17 *	* * *	root	cd / && run-parts --report /etc/cron.hourly

A line like this from /etc/crontab is exactly what runs the scripts from /etc/cron.daily/, /etc/cron.weekly/, and similar folders. Packages drop their jobs there (on Ubuntu, for example, apt-compat and dpkg in cron.daily).

systemd timers as an alternative

On hosts with systemd, timers play the same role: a .timer unit runs a .service unit of the same name on a schedule. Example for a backup every day at 03:00:

# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

How this differs from cron:

cron is simpler and is almost everywhere, including minimal systems. timers need systemd and two files, but give you logs, dependencies, and resource limits out of the box. The unit files themselves were covered in the systemd lesson.

Common mistakes

What you'll need for the tasks

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson linux-admin-04-cron