open atlas
↑ Back to track
Linux, the operating system LIN · 11 · 01

cron and crontab

cron schedules commands with a five-field expression (minute hour dom month dow). User crontabs live under /var/spool/cron; system jobs in /etc/cron.d. The cron environment is minimal — bare PATH, no profile — which is the top source of "works in my shell, fails in cron".

LIN Junior ◷ 20 min
Level
FoundationsJuniorMiddleSenior

Every system has work that must repeat: nightly backups, hourly log rotations, a health-check ping every five minutes. cron has been the answer since 1975. You write a line like 0 3 * * * /usr/bin/backup.sh and the daemon wakes up every minute, checks whether any job is due, and runs it. Simple. But cron has one nasty habit: it runs jobs in a stripped-down environment — no profile, no bashrc, a bare PATH that knows nothing about /usr/local/bin or your language runtimes. A script that works perfectly in your shell will silently fail in cron for exactly that reason. Understanding the five time fields is five minutes of learning; understanding the environment trap saves you hours of debugging.

Goal

After this lesson you can write a crontab entry with all five time fields, edit your user crontab with crontab -e, add a system job in /etc/cron.d, use @reboot and @daily shortcuts, and avoid the minimal-PATH trap that breaks most first-time cron jobs.

1

The five time fields control when the job runs. A crontab line has five time fields followed by the command:

# m  h  dom  mon  dow  command
# |  |   |    |    |
# |  |   |    |    +--- day of week (0=Sun, 6=Sat; 7=Sun too)
# |  |   |    +-------- month (1-12)
# |  |   +------------- day of month (1-31)
# |  +----------------- hour (0-23)
# +-------------------- minute (0-59)

# Run at 03:00 every night:
0 3 * * * /usr/bin/backup.sh

# Run at minute 0 and 30 every hour (two values, comma-separated):
0,30 * * * * /usr/bin/check-health.sh

# Run every 15 minutes (step syntax with /):
*/15 * * * * /usr/bin/poll-queue.sh

# Run at 08:00 on weekdays only (Mon-Fri):
0 8 * * 1-5 /usr/bin/morning-report.sh

An asterisk * means “every”. A comma separates values. A slash /N means “every N”. A dash - creates a range. These four constructs cover almost every schedule you will ever need.

2

Edit your personal crontab with crontab -e. Each user has their own crontab. The daemon runs jobs as that user, with that user’s permissions — no password required, no sudo needed for user-owned tasks.

# Open your crontab in $EDITOR (vi by default):
crontab -e

# List the current crontab without editing:
crontab -l

# Remove your entire crontab (careful — no undo):
crontab -r

# Edit another user's crontab (root only):
crontab -u alice -e

The file is stored under /var/spool/cron/crontabs/<username> — never edit it directly; always go through crontab -e, which validates syntax before installing.

3

System-wide jobs live in /etc/cron.d/ and /etc/crontab. These files add a sixth field — the username — because the daemon needs to know which user to run the job as.

# /etc/crontab and /etc/cron.d files have a USER field between the time fields and command:
# m  h  dom  mon  dow  USER    command
0    2  *    *    *    root    /usr/sbin/logrotate /etc/logrotate.conf

# In /etc/cron.d/ you can drop separate files per package:
ls /etc/cron.d/
# php   apt-compat   sysstat   unattended-upgrades

# Check what Ubuntu's own daily/weekly/monthly jobs look like:
ls /etc/cron.daily/   # scripts, executed by run-parts
ls /etc/cron.weekly/
ls /etc/cron.monthly/

The /etc/cron.daily/ directories are run by run-parts — any executable script in there fires at the scheduled time. No crontab syntax required; just drop a script and make it executable.

4

@reboot, @daily, and friends replace the five fields for common schedules. These are shorthand strings that cron translates internally:

# Equivalent shorthands:
@reboot    /usr/bin/start-agent.sh       # Run once, when the daemon starts
@hourly    /usr/bin/check-disk.sh        # 0 * * * *
@daily     /usr/bin/nightly-backup.sh    # 0 0 * * *
@weekly    /usr/bin/weekly-cleanup.sh    # 0 0 * * 0
@monthly   /usr/bin/monthly-report.sh    # 0 0 1 * *
@yearly    /usr/bin/audit.sh             # 0 0 1 1 *

# @reboot is the most useful shorthand: replaces /etc/rc.local for user-level
# startup commands without needing a systemd unit.

@reboot fires when crond starts, not strictly when the machine boots — but on Ubuntu with crond enabled, crond starts at boot, so they are functionally equivalent for most uses.

5

The cron environment is minimal — this is the #1 failure mode. When cron runs your command, it sets a bare environment: SHELL=/bin/sh, PATH=/usr/bin:/bin, HOME and LOGNAME from the password file. That is it. No .bashrc, no .profile, no nvm, no pyenv, no /usr/local/bin.

# A job that fails silently because 'node' is not in /usr/bin:/bin:
*/5 * * * * node /opt/app/worker.js

# Fix 1: use the absolute path to the binary:
*/5 * * * * /usr/local/bin/node /opt/app/worker.js

# Fix 2: override PATH at the top of the crontab (applies to all jobs below):
PATH=/usr/local/bin:/usr/bin:/bin
*/5 * * * * node /opt/app/worker.js

# Fix 3: source the profile explicitly in the command:
*/5 * * * * bash -l -c 'node /opt/app/worker.js'

# Capture stdout AND stderr to a log file for debugging:
*/5 * * * * /usr/local/bin/node /opt/app/worker.js >> /var/log/worker.log 2>&1

The >> /var/log/worker.log 2>&1 tail is the single most useful addition to any cron job: without it, output is mailed to the user (if a mail agent is running) or silently discarded.

Worked example

Add a daily database dump that actually works in cron.

Goal: dump a PostgreSQL database every day at 02:30, write to /var/backups/db/, keep 7 days of files.

# Edit the postgres user's crontab:
sudo crontab -u postgres -e

# Add these lines (the top two set the environment; the job line follows):
PATH=/usr/bin:/bin
PGPASSFILE=/var/lib/postgresql/.pgpass

30 2 * * * pg_dump mydb | gzip > /var/backups/db/mydb-$(date +\%Y\%m\%d).sql.gz
35 2 * * * find /var/backups/db/ -name 'mydb-*.sql.gz' -mtime +7 -delete

Three things to note:

  1. PATH is set explicitly — pg_dump is in /usr/bin on Debian, so this would work without it here, but it is a habit worth building.
  2. date +\%Y\%m\%d uses \% because % is a special character in crontab syntax — it is translated to a newline. Any literal % in a crontab command must be escaped as \%.
  3. The cleanup job at 02:35 uses find -mtime +7 — files older than 7 days. Running it 5 minutes after the dump ensures the fresh dump is not deleted.
Common mistake

The % escape trap. In crontab syntax, an unescaped % is translated into a newline, and everything after the first % becomes stdin for the command. This means date +%Y%m%d in a crontab line becomes date + followed by a newline and then Y etc. as stdin — the date command sees only the format string up to the first % and produces wrong output or an error. Always write \% in crontab: date +\%Y\%m\%d. This trap hits every engineer once, usually in a backup script where a corrupted filename is the only symptom.

Why this works

macOS and cron. On macOS, cron is present but officially deprecated since macOS 10.4 (Tiger). Apple’s replacement is launchd and launchctl with .plist files in ~/Library/LaunchAgents/ for user jobs. The syntax is XML-based and verbose, but it integrates with macOS power management (jobs can wake the machine). For cross-platform shell scripts that also run on macOS servers, consider whether the job truly needs macOS-native scheduling or whether you can deploy it on a Linux host instead.

Check yourself
Quiz

You write this crontab entry: `*/10 * * * * python3 /opt/app/sync.py`. The script runs fine from your shell but never produces output in cron. What is the most likely cause?

Recap

A crontab entry has five time fields — minute, hour, day-of-month, month, day-of-week — followed by the command. Use * for “every”, , for multiple values, /N for step, - for range. Edit your personal crontab with crontab -e; system jobs with a username field go in /etc/cron.d/. @reboot and @daily are readable shortcuts for common schedules. The most important rule: cron runs in a minimal environmentPATH=/usr/bin:/bin, no profile — so always use absolute paths to binaries or set PATH at the top of the crontab. Redirect output with >> logfile 2>&1 to make failures visible. Escape literal % as \% in cron commands.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.