Cron has been scheduling Unix tasks since 1975. It’s simple, reliable, and will silently fail in ways that waste hours of debugging. Here’s how to use it properly.

Cron Syntax

comDMDHMmaoaoiaynyunntrudohotff(e(0w1m-(e-o20e1n3-k2t)5)h9()0(-17-,310)and7areSunday)

Common Schedules

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Every minute
* * * * * /script.sh

# Every 5 minutes
*/5 * * * * /script.sh

# Every hour at minute 0
0 * * * * /script.sh

# Daily at 3 AM
0 3 * * * /script.sh

# Weekly on Sunday at midnight
0 0 * * 0 /script.sh

# Monthly on the 1st at 6 AM
0 6 1 * * /script.sh

# Every weekday at 9 AM
0 9 * * 1-5 /script.sh

# Every 15 minutes during business hours
*/15 9-17 * * 1-5 /script.sh

The Silent Failure Problem

Cron’s default behavior: run command, discard output, send errors to email (which you probably haven’t configured).

Always redirect output:

1
2
3
4
5
6
7
8
# Log everything
0 3 * * * /backup.sh >> /var/log/backup.log 2>&1

# Log with timestamp
0 3 * * * /backup.sh 2>&1 | ts '[%Y-%m-%d %H:%M:%S]' >> /var/log/backup.log

# Discard output only if you really don't care
0 * * * * /cleanup.sh > /dev/null 2>&1

Environment Gotcha

Cron runs with a minimal environment. Your script works in terminal but fails in cron because:

  • PATH is minimal (/usr/bin:/bin)
  • No user profile loaded
  • No environment variables from .bashrc

Fix 1: Use full paths

1
2
3
4
5
# Bad
0 * * * * python /scripts/task.py

# Good
0 * * * * /usr/bin/python3 /scripts/task.py

Fix 2: Set PATH in crontab

1
2
3
4
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash

0 * * * * /scripts/task.sh

Fix 3: Source environment in script

1
2
3
#!/bin/bash
source /home/user/.bashrc
# rest of script

Lock Files (Prevent Overlap)

If a job runs longer than its interval, you get overlapping runs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash
LOCKFILE=/tmp/myjob.lock

# Exit if already running
if [ -f "$LOCKFILE" ]; then
    echo "Already running"
    exit 1
fi

# Create lock
trap "rm -f $LOCKFILE" EXIT
touch "$LOCKFILE"

# Your actual work here
/do/the/thing

Or use flock:

1
0 * * * * flock -n /tmp/myjob.lock /scripts/myjob.sh

The -n flag makes it fail immediately if lock is held.

Timeout Protection

Prevent runaway jobs:

1
0 * * * * timeout 3600 /scripts/long-job.sh

Kills the job after 1 hour.

Error Notification

Get notified when things fail:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash
set -e  # Exit on error

cleanup() {
    if [ $? -ne 0 ]; then
        echo "Job failed at $(date)" | mail -s "Cron failure: backup" admin@example.com
    fi
}
trap cleanup EXIT

# Your job here
/run/backup

Or use a monitoring service:

1
0 3 * * * /backup.sh && curl -s https://hc-ping.com/your-uuid

Logging Best Practices

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/bash
LOG=/var/log/myjob.log
exec 1>>$LOG 2>&1  # Redirect all output to log

echo "=== Started at $(date) ==="

# Your commands here
/do/stuff

echo "=== Finished at $(date) ==="

Rotate logs:

1
2
3
4
5
6
7
8
# /etc/logrotate.d/myjob
/var/log/myjob.log {
    weekly
    rotate 4
    compress
    missingok
    notifempty
}

Testing Cron Jobs

Test the command manually first:

1
2
# Run exactly as cron would
env -i /bin/bash -c '/path/to/script.sh'

Check cron is running:

1
2
3
systemctl status cron
# or
systemctl status crond  # RHEL/CentOS

Watch the logs:

1
2
3
tail -f /var/log/syslog | grep CRON
# or
journalctl -u cron -f

List your crontab:

1
crontab -l

User vs System Crontabs

User crontab (crontab -e):

  • Stored in /var/spool/cron/crontabs/
  • Runs as that user
  • No user field in syntax

System crontab (/etc/crontab):

  • Has extra user field
  • Runs as specified user
1
2
3
4
5
# /etc/crontab has user field
0 3 * * * root /scripts/backup.sh

# User crontab doesn't
0 3 * * * /scripts/backup.sh

Drop-in directory (/etc/cron.d/):

  • Like system crontab (has user field)
  • One file per job
  • Easier to manage with config management

Cron Alternatives

Systemd timers (modern Linux):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target
1
2
3
4
5
6
7
# /etc/systemd/system/backup.service
[Unit]
Description=Backup job

[Service]
Type=oneshot
ExecStart=/scripts/backup.sh
1
2
systemctl enable --now backup.timer
systemctl list-timers

Benefits: better logging, dependency management, resource controls.

Common Mistakes

Wrong time zone:

1
2
3
# Set in crontab
TZ=America/New_York
0 9 * * * /morning-job.sh

Day-of-month AND day-of-week:

1
2
3
4
# This runs if EITHER matches (not both!)
0 0 15 * 5  # 15th of month OR Fridays

# For "15th if it's a Friday", use a wrapper script

Forgetting the newline: Crontab needs a trailing newline after the last entry.

Special characters in commands:

1
2
# Percent signs need escaping
0 * * * * echo "$(date +\%Y-\%m-\%d)" >> /log

Production Checklist

  • Full paths for all commands
  • Output redirected to log file
  • Lock file prevents overlap
  • Timeout prevents runaway
  • Error notification configured
  • Log rotation set up
  • Tested with minimal environment
  • Documented what the job does

Cron is a 50-year-old tool that still runs critical infrastructure. Respect it by setting it up properly.