Linux Cron Jobs: A Practical Guide

Cron is one of those tools that’s been around so long and is so widely used that most people learn just enough to make it work, then stop. But there are enough gotchas and useful features that it’s worth understanding properly.

The Cron Daemon and Config Files

crond (or cron on some systems) is the daemon that reads job schedules and executes them. Configuration lives in several places:

  • /etc/crontab — system-wide crontab (includes a user field)
  • /etc/cron.d/ — individual drop-in files (same format as /etc/crontab)
  • /var/spool/cron/crontabs/username — per-user crontabs (edited with crontab -e)
  • /etc/cron.daily/, /etc/cron.hourly/, /etc/cron.weekly/, /etc/cron.monthly/ — scripts that run at those intervals

Crontab Format

# m h dom mon dow command
  * * *  *   *   /usr/local/bin/myscript.sh

Five fields before the command:

  • m — minute (0–59)
  • h — hour (0–23)
  • dom — day of month (1–31)
  • mon — month (1–12 or names)
  • dow — day of week (0–7, where both 0 and 7 are Sunday, or names)

Special values:

  • * — every value
  • */5 — every 5 (step)
  • 1-5 — range
  • 1,3,5 — list

Some examples:

# Every minute
* * * * * /path/to/command

# Every 5 minutes
*/5 * * * * /path/to/command

# Daily at 2:30am
30 2 * * * /path/to/command

# Every Monday at 9am
0 9 * * 1 /path/to/command

# First day of each month at midnight
0 0 1 * * /path/to/command

# Weekdays at 8am
0 8 * * 1-5 /path/to/command

# Every 15 minutes between 9am-5pm on weekdays
*/15 9-17 * * 1-5 /path/to/command

Editing Your Crontab

crontab -e    # Edit your crontab (opens $EDITOR)
crontab -l    # List current crontab
crontab -r    # Remove your crontab (careful — no confirmation)

For the system crontab or drop-ins, edit the files directly:

vim /etc/cron.d/myapp

The /etc/cron.d/ format includes a username field:

30 2 * * * root /usr/local/bin/backup.sh

The user field (root in this case) tells cron which user to run the command as.

The PATH Problem

This is the source of most cron problems. Cron runs with a minimal environment and a very limited PATH. Commands that work in your terminal fail in cron because the executable isn’t in cron’s PATH.

Solutions:

Use full paths:

0 3 * * * /usr/bin/python3 /home/deploy/scripts/report.py

Set PATH in the crontab:

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

0 3 * * * report.py

Source your profile in a wrapper script:

#!/bin/bash
source /etc/profile
source ~/.bashrc
/home/deploy/scripts/report.py

I use full paths by default. It’s explicit and eliminates ambiguity.

Handling Output

By default, cron emails output (stdout + stderr) to the user. This usually means it goes nowhere (root’s email isn’t read on most servers) or clogs your mail spool.

Redirect to a log file:

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

>> appends to the log file, 2>&1 redirects stderr to stdout (so both go to the log).

Or suppress all output:

0 3 * * * /usr/local/bin/backup.sh > /dev/null 2>&1

I avoid suppressing output — if a job fails, you want to know why. Log to a file and set up log rotation.

For email notification of failures only, use a wrapper:

#!/bin/bash
output=$(/usr/local/bin/backup.sh 2>&1)
if [ $? -ne 0 ]; then
    echo "$output" | mail -s "Backup failed" you@example.com
fi

Preventing Overlapping Runs

If a cron job takes longer than its interval, multiple instances can stack up. For jobs that shouldn’t overlap (backups, database maintenance), use flock:

0 * * * * flock -n /tmp/myapp.lock /usr/local/bin/myapp.sh >> /var/log/myapp.log 2>&1

flock -n acquires a lock or exits immediately if already locked. The -n means non-blocking — if the lock is held, the new instance exits rather than waiting.

Or use a PID file approach:

#!/bin/bash
PIDFILE=/tmp/myapp.pid

if [ -f "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then
    echo "Already running"
    exit 1
fi

echo $$ > "$PIDFILE"
trap "rm -f $PIDFILE" EXIT

# ... your script here ...

Environment Variables in Crontab

Set variables at the top of the crontab:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=""

0 3 * * * /usr/local/bin/backup.sh

MAILTO="" disables email output. SHELL=/bin/bash ensures bash syntax works (default cron shell varies by system).

Testing Cron Jobs

The most reliable test is to run the command manually as the correct user:

# Test as yourself
/usr/local/bin/backup.sh

# Test as root (if the cron runs as root)
sudo /usr/local/bin/backup.sh

# Test with cron's environment (minimal PATH)
env -i /usr/local/bin/backup.sh

The env -i version simulates cron’s minimal environment more closely. If it fails there but works normally, you have an environment issue.

For scheduled testing, add a temporary job that runs in a few minutes:

# In crontab: run once, 3 minutes from now
$(date -d "+3 minutes" +%M\ %H)\ * * * /usr/local/bin/backup.sh >> /tmp/test.log 2>&1

Check the log after it runs, then remove the test entry.

anacron for Servers That Aren’t Always On

Cron misses jobs when the system is off at the scheduled time. anacron runs jobs based on how long since they last ran, not at specific times. It’s designed for systems that aren’t always on.

cat /etc/anacrontab
# period  delay  job-identifier   command
1         5      cron.daily       run-parts --report /etc/cron.daily
7         10     cron.weekly      run-parts --report /etc/cron.weekly
@monthly  15     cron.monthly     run-parts --report /etc/cron.monthly

period is days between runs. delay is minutes to wait after boot before running. On a desktop or laptop, anacron ensures daily tasks eventually run even if the machine was off at the usual time.

For servers, systemd timers with Persistent=true are a better alternative to anacron — they’re integrated with the systemd ecosystem and have better logging. The systemd timers guide covers this in detail.

Checking if Cron Is Running

systemctl status cron    # Ubuntu/Debian
systemctl status crond   # CentOS/RHEL

Check recent cron activity:

grep CRON /var/log/syslog | tail -20    # Debian/Ubuntu
grep CRON /var/log/cron | tail -20      # CentOS/RHEL

Each cron execution shows in the log. If your expected jobs aren’t appearing, that’s a sign something’s wrong with the crontab format or the cron daemon itself.

Useful crontab Shortcuts

Some implementations support shortcuts:

@reboot    /usr/local/bin/startup.sh    # Run once at boot
@hourly    /usr/local/bin/hourly.sh     # Run once per hour
@daily     /usr/local/bin/daily.sh      # Run once per day at midnight
@weekly    /usr/local/bin/weekly.sh     # Run once per week
@monthly   /usr/local/bin/monthly.sh    # Run once per month

@reboot is particularly useful for starting custom services, setting up networking, or running initialization scripts on boot.

Scroll to Top