Cron Jobs Explained
Master cron job scheduling on Linux: full crontab syntax, environment variables, output logging, and when to use systemd timers instead.
Before you start
- ▸Terminal access with a normal user account
- ▸sudo privileges for system-wide crontabs and systemd timers
- ▸crond or cron package installed and running (pre-installed on most distros)
Cron is the classic Unix job scheduler, and it still runs on virtually every Linux system. Whether you need to rotate logs at midnight, sync files every five minutes, or send a weekly report, cron handles it with a single line of text. This guide covers the full crontab syntax, common pitfalls around environment variables, how to read cron logs, and when to reach for the modern alternative: systemd timers.
How Cron Works
The crond daemon wakes up every minute and checks for jobs whose schedule matches the current time. Jobs are stored in crontabs — plain text files, one per user — plus a set of system-wide files under /etc/cron.d/, /etc/cron.daily/, and similar directories.
Each user manages their own crontab with the crontab command. You never edit the file directly; the command validates and installs it atomically.
Editing Your Crontab
Open your personal crontab in your default editor:
crontab -e
To set a specific editor first:
export VISUAL=nano
crontab -e
List the current crontab without editing:
crontab -l
Remove your entire crontab (careful — no confirmation prompt):
crontab -r
Root can manage another user's crontab:
sudo crontab -u alice -e
Crontab Syntax
Every job line has six fields: five time fields followed by the command.
# ┌─────────── minute (0-59)
# │ ┌───────── hour (0-23)
# │ │ ┌─────── day of month (1-31)
# │ │ │ ┌───── month (1-12 or jan-dec)
# │ │ │ │ ┌─── day of week (0-7, both 0 and 7 = Sunday, or sun-sat)
# │ │ │ │ │
# * * * * * command to run
Special Characters
- * — every unit ("every minute", "every hour", etc.)
- , — list of values:
1,15,30 - - — range:
9-17means hours 9 through 17 - / — step:
*/5means every 5 units;0-30/10means 0, 10, 20, 30
Practical Schedule Examples
| Schedule | Meaning |
|---|---|
* * * * * | Every minute |
0 * * * * | Every hour on the hour |
30 6 * * * | Daily at 06:30 |
0 2 * * 0 | Every Sunday at 02:00 |
*/15 * * * * | Every 15 minutes |
0 9-17 * * 1-5 | On the hour, 9 AM – 5 PM, weekdays only |
0 0 1 * * | First day of every month at midnight |
0 0 1 1 * | Once a year, January 1 at midnight |
Cron also accepts shorthand aliases: @reboot, @hourly, @daily, @weekly, @monthly, and @yearly. Use @reboot to run something once on startup.
# Example: clear the /tmp/myapp cache on every reboot
@reboot rm -rf /tmp/myapp/cache/*
# Example: run a backup script every day at 2 AM
0 2 * * * /usr/local/bin/backup.sh
Environment Variables in Cron
This is where most cron jobs break. Cron does not load your shell's profile or .bashrc. The default environment is minimal — typically just HOME, LOGNAME, SHELL, and a stripped-down PATH that often looks like /usr/bin:/bin. Commands that work in your terminal fail silently in cron because the binary isn't on that PATH.
The safest fixes:
- Use absolute paths for every command and every file:
/usr/local/bin/python3, notpython3. - Set
PATHexplicitly at the top of your crontab:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
0 3 * * * /usr/local/bin/backup.sh
MAILTO="" suppresses the local email cron sends for any command that produces output. If you want email alerts, set it to a real address and ensure a mail transport agent is installed — or redirect output to a log file instead (shown below).
You can also set variables per-line by wrapping the command in a shell that loads your profile:
0 4 * * * bash -l -c '/usr/local/bin/myscript.sh'
Redirecting Output and Logging
By default, cron mails stdout and stderr to the local user. Most servers have no mail reader, so output disappears. Always redirect explicitly:
# Append stdout and stderr to a log file
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Discard all output
*/5 * * * * /usr/local/bin/heartbeat.sh > /dev/null 2>&1
Rotating those logs is wise. Drop a config file into logrotate:
sudo nano /etc/logrotate.d/backup
/var/log/backup.log {
weekly
rotate 4
compress
missingok
notifempty
}
Reading System Cron Logs
On Debian/Ubuntu, cron events go to syslog:
grep CRON /var/log/syslog | tail -20
On Fedora, RHEL, Rocky, and Arch where journald is the primary log sink:
journalctl -u cron -f # Debian/Ubuntu service name
journalctl -u crond -f # Fedora/RHEL/Rocky
journalctl -t CROND --since today
System-Wide Crontabs
Files dropped in /etc/cron.d/ follow the same syntax but include an extra username field between the schedule and the command, because they run as root's cron daemon:
# /etc/cron.d/cleanup
PATH=/usr/local/bin:/usr/bin:/bin
# minute hour dom month dow user command
0 3 * * * root /usr/local/bin/cleanup.sh >> /var/log/cleanup.log 2>&1
Scripts placed directly in /etc/cron.daily/, /etc/cron.weekly/, etc. just need to be executable — no time fields required. The system run-parts mechanism calls them at a time configured in /etc/crontab.
systemd Timers: The Modern Alternative
systemd timers are more capable than cron: they can depend on other units, catch up missed runs (persistence), log natively to the journal, and integrate cleanly with sandboxing. For new long-lived infrastructure work, prefer timers. For quick personal scripts and portability, cron is still fine.
A timer requires two unit files: a .service (what to run) and a .timer (when to run it).
sudo nano /etc/systemd/system/backup.service
[Unit]
Description=Daily backup script
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=alice
sudo nano /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 02:00
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Enable and start the timer:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
Check its status and next trigger:
systemctl list-timers --all | grep backup
Output will show something like:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sun 2025-06-15 02:00:00 UTC 7h left Sat 2025-06-14 02:00:01 UTC 16h ago backup.timer backup.service
Persistent=true means if the machine was off at 02:00, the job runs shortly after the next boot — something vanilla cron cannot do.
Verification
Test a cron job immediately by running the exact command from a shell with a stripped PATH (mimicking cron's environment):
env -i HOME=$HOME LOGNAME=$USER PATH=/usr/bin:/bin SHELL=/bin/sh /usr/local/bin/backup.sh
If that fails, your script has an environment problem. Fix it before adding it to crontab. After adding, check the log or journal in the next few minutes to confirm execution.
Troubleshooting
- Job never runs: Confirm crond/cron is active (
systemctl status cronorsystemctl status crond). Check the log for parse errors right aftercrontab -e. - Command not found: Set an explicit PATH in the crontab or use absolute paths everywhere.
- Job runs but does nothing: Redirect stderr to a log file (
2>&1) — the real error is hidden in stderr. - Permission denied on a script: The script must be executable:
chmod +x /usr/local/bin/backup.sh. - Day-of-month and day-of-week: If both are set to non-
*values, cron treats them as OR, not AND. This surprises people. Use a wrapper script with date logic for precise AND conditions. - Fedora/RHEL — crond not installed: Install with
sudo dnf install cronie && sudo systemctl enable --now crond.
Frequently asked questions
- Why does my cron job work in the terminal but fail when cron runs it?
- Cron uses a minimal environment without your shell profile. The most common cause is a command not found on cron's restricted PATH. Use absolute paths for every binary, or set PATH explicitly at the top of your crontab.
- How do I run a cron job every 5 minutes?
- Use `*/5 * * * * /path/to/command`. The `/` step syntax means "every 5 units of the minute field", so this fires at 0, 5, 10, 15 … 55 each hour.
- What is the difference between cron and systemd timers?
- Both schedule recurring tasks, but systemd timers log to the journal, support dependencies on other units, can catch up missed runs with Persistent=true, and integrate with systemd's sandboxing features. Cron is simpler and works on any Unix-like system without systemd.
- Can I run a cron job as a different user?
- Root can run `crontab -u username -e` to edit another user's crontab. Alternatively, system-wide jobs in /etc/cron.d/ include a username field in each line that specifies which user runs the command.
- How do I stop cron from sending email for every job that produces output?
- Add `MAILTO=""` at the top of your crontab to suppress all mail. Alternatively, redirect output to a log file with `>> /var/log/myjob.log 2>&1` on each command line.
Related guides
Bash Arrays and Associative Arrays
Master bash indexed and associative arrays: declaration, element access, looping, mapfile, namerefs, and practical patterns for real scripting work.
Bash Functions and Variable Scoping
Master Bash function scoping with local variables, source-based libraries, correct use of return codes, and array passing techniques including namerefs.
Bash Loops: for, while and until
Learn all three Bash loop types — for, while, and until — with practical, copy-paste examples covering file iteration, counting, polling, and safe line reading.
Bash Scripting for Beginners
Learn Bash scripting from scratch: shebang lines, variables, conditionals, loops, and arguments, plus a real backup script to tie it all together.