Cron has been scheduling tasks on Unix systems since 1975. It’s simple, reliable, and available everywhere. But that simplicity hides gotchas that break jobs in production.

Cron Syntax

commamnidnhuotuderaym((o00ond--fta52hy93m))o(on1ft-h1w2e()e1k-3(10)-7,0and7areSunday)

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
* * * * * /path/to/script.sh

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

# Every day at midnight
0 0 * * * /path/to/script.sh

# Every day at 2:30 AM
30 2 * * * /path/to/script.sh

# Every Monday at 9 AM
0 9 * * 1 /path/to/script.sh

# Every 15 minutes
*/15 * * * * /path/to/script.sh

# Every weekday at 6 PM
0 18 * * 1-5 /path/to/script.sh

# First day of every month at midnight
0 0 1 * * /path/to/script.sh

Special Strings

1
2
3
4
5
6
7
8
@reboot     # Run once at startup
@yearly     # 0 0 1 1 *
@annually   # Same as @yearly
@monthly    # 0 0 1 * *
@weekly     # 0 0 * * 0
@daily      # 0 0 * * *
@midnight   # Same as @daily
@hourly     # 0 * * * *

Editing Crontabs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Edit current user's crontab
crontab -e

# List current user's crontab
crontab -l

# Edit another user's crontab (as root)
crontab -u username -e

# Remove all cron jobs (careful!)
crontab -r

System Crontabs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# System-wide crontab (includes user field)
/etc/crontab

# Drop-in directory (no user field needed)
/etc/cron.d/

# Periodic directories (scripts run by run-parts)
/etc/cron.hourly/
/etc/cron.daily/
/etc/cron.weekly/
/etc/cron.monthly/

/etc/crontab format includes username:

1
0 * * * * root /path/to/script.sh

The Environment Trap

Cron runs with a minimal environment. Your script works interactively but fails in cron? Environment.

Set PATH Explicitly

1
2
3
4
5
6
7
8
9
# Bad - relies on user PATH
* * * * * backup.sh

# Good - explicit path
* * * * * /usr/local/bin/backup.sh

# Or set PATH in crontab
PATH=/usr/local/bin:/usr/bin:/bin
* * * * * backup.sh

In Your Script

1
2
3
4
5
#!/bin/bash
export PATH=/usr/local/bin:/usr/bin:/bin
export HOME=/home/myuser

# Now your script has the expected environment

Load Profile

1
2
3
4
5
# Load user's environment
* * * * * . ~/.profile; /path/to/script.sh

# Or bash login shell
* * * * * bash -l -c '/path/to/script.sh'

Logging and Debugging

Capture Output

1
2
3
4
5
# Redirect stdout and stderr to file
* * * * * /path/to/script.sh >> /var/log/myjob.log 2>&1

# With timestamp
* * * * * /path/to/script.sh >> /var/log/myjob.log 2>&1; echo "---$(date)---" >> /var/log/myjob.log

Email Output

By default, cron emails output to the user. Control this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Send to specific email
MAILTO=alerts@example.com
* * * * * /path/to/script.sh

# Disable email
MAILTO=""
* * * * * /path/to/script.sh

# Only email on error
* * * * * /path/to/script.sh > /dev/null

Test Your Job

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

# Check cron logs
grep CRON /var/log/syslog
journalctl -u cron

Preventing Overlapping Runs

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

Using flock

1
* * * * * flock -n /tmp/myjob.lock /path/to/script.sh

Options:

  • -n — Exit immediately if lock can’t be acquired
  • -w 10 — Wait up to 10 seconds for lock
  • -x — Exclusive lock (default)

In Your Script

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

if ! mkdir "$LOCKFILE" 2>/dev/null; then
    echo "Already running"
    exit 1
fi

trap "rm -rf $LOCKFILE" EXIT

# Your job here

Error Handling

Exit on Error

1
2
3
4
#!/bin/bash
set -e  # Exit on any error

# Commands here

Notify on Failure

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

cleanup() {
    if [ $? -ne 0 ]; then
        curl -X POST https://hooks.slack.com/... \
            -d '{"text":"Cron job failed!"}'
    fi
}
trap cleanup EXIT

# Your job here

Wrapper Script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/bash
# /usr/local/bin/cron-wrapper

JOB_NAME="$1"
shift

if ! "$@"; then
    echo "[$JOB_NAME] Failed at $(date)" | mail -s "Cron Failure: $JOB_NAME" alerts@example.com
    exit 1
fi
1
* * * * * /usr/local/bin/cron-wrapper backup /path/to/backup.sh

Timezone Issues

Cron uses the system timezone. Be explicit:

1
2
3
# Set timezone in crontab
TZ=America/New_York
0 9 * * * /path/to/script.sh  # 9 AM Eastern

Or use UTC and convert:

1
2
# Run at 14:00 UTC (9 AM Eastern during EST)
0 14 * * * /path/to/script.sh

Random Delay

Avoid thundering herd when many servers run the same job:

1
2
3
4
# Random delay up to 5 minutes
* * * * * sleep $((RANDOM \% 300)) && /path/to/script.sh

# Or use systemd timer's RandomizedDelaySec

Best Practices

1. Use Absolute Paths

1
2
3
4
5
# Bad
* * * * * cd mydir && ./script.sh

# Good
* * * * * cd /home/user/mydir && ./script.sh

2. Log Everything

1
2
3
4
5
6
7
#!/bin/bash
exec >> /var/log/myjob.log 2>&1
echo "=== Starting at $(date) ==="

# Your job here

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

3. Set Timeouts

1
2
# Kill job if it runs longer than 1 hour
* * * * * timeout 3600 /path/to/script.sh

4. Use Dedicated User

1
2
3
4
5
# Create service account
sudo useradd -r -s /usr/sbin/nologin cronjobs

# Set up crontab for that user
sudo crontab -u cronjobs -e

5. Monitor Job Status

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash
START=$(date +%s)

# Your job here
result=$?

END=$(date +%s)
DURATION=$((END - START))

# Send metrics
curl -X POST "https://metrics.example.com/cron" \
    -d "job=backup&duration=$DURATION&status=$result"

Systemd Timers (Modern Alternative)

For newer systems, systemd timers offer more features:

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

[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=300
Persistent=true

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

[Service]
Type=oneshot
ExecStart=/path/to/backup.sh
User=backup
Nice=10
IOSchedulingClass=idle
1
2
3
sudo systemctl enable backup.timer
sudo systemctl start backup.timer
systemctl list-timers

Quick Reference

ScheduleSyntax
Every minute* * * * *
Every 5 minutes*/5 * * * *
Every hour0 * * * *
Daily at midnight0 0 * * *
Daily at 3:30 AM30 3 * * *
Every Monday0 0 * * 1
First of month0 0 1 * *
Weekdays at 9 AM0 9 * * 1-5

Cron’s simplicity is its strength and weakness. The syntax is easy to learn, but the minimal environment trips up everyone at least once. Set your PATH, lock your jobs, log your output, and you’ll have cron jobs that run reliably for years.