KB
Linux Administration

Scheduling Tasks with Cron

6 min read1178 words11 code blocks

At a glance#

  • Purpose: Schedule recurring jobs on Linux with cron, and diagnose jobs that do not run.
  • Applies to: Any Linux distribution.
  • Risk: Medium — a badly written job can fill a disk, hammer a database, or run destructive commands unattended.
  • Time: 20 minutes.

Overview#

Cron runs commands on a schedule. It is simple, universally available, and the source of a specific family of frustrating faults — almost all of which come from the same root cause: cron does not run your shell environment.

Jobs work perfectly when tested by hand and then fail silently at 2am because PATH is minimal, no profile is sourced, and nobody is reading the output.

Crontab basics#

bash
# edit your own crontab
crontab -e

# list it
crontab -l

# edit another user's (as root)
sudo crontab -e -u www-data

# remove it entirely - no confirmation prompt
crontab -r
Warning: crontab -r deletes the crontab immediately with no confirmation, and it sits next to -e on the keyboard. Back up first: crontab -l > ~/crontab-backup-$(date +%F).txt

Schedule syntax#

text
* * * * * command
│ │ │ │ │
│ │ │ │ └── day of week  (0-7, 0 and 7 both = Sunday)
│ │ │ └──── month        (1-12)
│ │ └────── day of month (1-31)
│ └──────── hour         (0-23)
└────────── minute       (0-59)

Common patterns:

ScheduleMeaning
0 2 * * *Every day at 02:00
*/15 * * * *Every 15 minutes
0 */4 * * *Every 4 hours
30 3 * * 0Sundays at 03:30
0 0 1 * *First of the month at midnight
0 9-17 * * 1-5Hourly, 09:00–17:00, weekdays
@rebootOnce at boot
@dailyEquivalent to 0 0 * * *
Note: Day-of-month and day-of-week are OR, not AND. 0 0 13 * 5 runs on the 13th and every Friday, not only Friday the 13th. This surprises people.

Writing a job that actually works#

A crontab that avoids the common failures:

bash
# environment - cron gives you almost nothing by default
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=sysadmins@example.com

# nightly backup, all output captured
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

# clear old temp files weekly
30 3 * * 0 find /tmp -type f -mtime +7 -delete >> /var/log/tmpclean.log 2>&1

# certificate renewal check twice daily, at odd minutes
17 3,15 * * * /usr/bin/certbot renew --quiet >> /var/log/certbot.log 2>&1

Rules worth following every time:

  1. Use absolute paths for both the command and every file it touches.
  2. Redirect output with >> file 2>&1. Without it, output goes to mail — and if mail is not configured, it is discarded and you learn nothing.
  3. Set PATH explicitly at the top of the crontab.
  4. Escape % as \%. In crontab, an unescaped % becomes a newline, which silently truncates the command. This bites anyone using date +%F.
  5. Avoid round numbers. Everything scheduled at 0 0 runs simultaneously across the estate. Offsetting to 17 spreads the load.

The percent-sign trap#

bash
# WRONG - breaks at the first %
0 2 * * * tar -czf /backup/db-$(date +%F).tar.gz /var/lib/mysql

# RIGHT - escaped
0 2 * * * tar -czf /backup/db-$(date +\%F).tar.gz /var/lib/mysql

# BETTER - put it in a script and avoid the issue entirely
0 2 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1

Putting anything non-trivial in a script is the right answer. It is testable, version-controllable, and free of crontab escaping rules.

System-wide cron#

text
/etc/crontab            # system crontab - has an extra user field
/etc/cron.d/            # drop-in files, same format
/etc/cron.hourly/       # scripts run hourly
/etc/cron.daily/        # scripts run daily
/etc/cron.weekly/
/etc/cron.monthly/

System crontab entries include a user column:

text
# m h dom mon dow user  command
0 2 * * *   root   /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
bash
sudo nano /etc/cron.d/myapp
sudo chmod 644 /etc/cron.d/myapp
Warning: Files in /etc/cron.d/ must not have a file extension. Cron ignores anything containing a dot, so myapp.conf is silently skipped — a fault with no error message anywhere.

Scripts in cron.daily and similar must be executable and also have no extension:

bash
sudo chmod +x /etc/cron.daily/myjob

Preventing overlapping runs#

A job that takes longer than its interval will start again before the previous run finishes. flock prevents that:

bash
*/5 * * * * /usr/bin/flock -n /tmp/sync.lock /usr/local/bin/sync.sh >> /var/log/sync.log 2>&1

-n exits immediately if the lock is held, so the run is skipped rather than queued.

Checking that jobs ran#

bash
# RHEL family
sudo tail -50 /var/log/cron
sudo grep CRON /var/log/messages

# Debian / Ubuntu
sudo grep CRON /var/log/syslog
sudo journalctl -u cron --since today

# all crontabs on the system
sudo ls -la /var/spool/cron/          # RHEL
sudo ls -la /var/spool/cron/crontabs/ # Debian

Cron logs that it started a job. It does not log whether the job succeeded — that is why redirecting output to a log file matters.

systemd timers as an alternative#

For anything important, systemd timers are better: real logging, dependency handling, and missed-run recovery. See Managing Services with systemd.

cronsystemd timer
SetupOne lineTwo unit files
LoggingWhatever you redirectAutomatic, in the journal
Missed runsLostPersistent=true catches up
DependenciesNoneFull systemd ordering
AvailabilityEverywheresystemd systems only

Use cron for simple, low-stakes jobs; timers for anything whose failure matters.

Verification#

bash
# job is registered
crontab -l
sudo cat /etc/cron.d/myapp

# script runs correctly by hand, as the right user
sudo -u www-data /usr/local/bin/myjob.sh

# simulate cron's minimal environment - this is the real test
env -i /bin/bash --noprofile --norc -c '/usr/local/bin/myjob.sh'

# confirm cron started it
sudo grep myjob /var/log/cron

# confirm the job's own log shows success
tail -20 /var/log/myjob.log

The env -i test is the one that catches the classic failure. A script that works in your shell but fails under env -i will fail under cron for exactly the same reason.

Troubleshooting#

SymptomCause and fix
Job never runsCron service stopped. systemctl status crond (RHEL) or cron (Debian).
Works manually, fails in cronEnvironment. Use absolute paths and set PATH at the top of the crontab.
command not found in the logPATH too minimal. Use the full path to the binary.
Job truncated at a %Unescaped percent sign. Use \% or move it into a script.
File in /etc/cron.d ignoredFilename contains a dot, or permissions are not 644.
Script in cron.daily ignoredNot executable, or has a file extension.
Runs at the wrong timeServer timezone. Check timedatectl; cron uses system local time.
Multiple copies runningJob overruns its interval. Wrap with flock.
No output anywhereNot redirected, and no local mail. Add >> log 2>&1.
Ran but did nothingCheck the job's own log, not the cron log. Cron only records that it started.