Skip to content

Cron setup: a practical walkthrough

Cron is the standard job scheduler in Linux, found in every infrastructure. Jobs pile up, logs accumulate, and the environment breaks things. Let’s walk through how to configure cron reliably and where it falls short.

When to Use Cron and When to Avoid It

Cron fits simple recurring tasks: backups, log rotation, temp file cleanup, periodic notifications. It’s a daemon that sleeps between runs — no resource consumption.

Avoid cron for:

  • tasks requiring millisecond precision (cron fires at the minute level)
  • tasks with hard dependencies on other services (use systemd units with After=)
  • long-running processes that could overlap (you need locking)

Anatomy of a Crontab Entry

Format:

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

Special values:

@reboot   — at boot
@yearly   — once a year (0 0 1 1 *)
@monthly  — once a month (0 0 1 * *)
@weekly   — once a week (0 0 * * 0)
@daily    — once a day (0 0 * * *)
@hourly   — once an hour (0 * * * *)

Examples:

0 3 * * *          /opt/scripts/backup.sh       # daily at 03:00
15,45 * * * *      /usr/local/bin/check.sh      # every 15 and 45 minutes
0 */4 * * *        /opt/metrics/collect.sh       # every 4 hours
0 9-17 * * 1-5     /opt/reports/daily.sh         # hourly during business hours on weekdays

crontab -e and crontab -l: Common Commands

crontab -l              # show current user crontab
crontab -e              # edit crontab (opens in EDITOR)
crontab -r              # remove crontab (no confirmation!)
crontab -l -u username  # view another user's crontab (from root)
crontab filename        # load jobs from file
Note

Default editor is vi. Change it: export EDITOR=nano.

Where System Schedules Live

Beyond user crontabs, there are system files:

/etc/crontab                # system crontab (format differs — includes username field)
/etc/cron.d/                # drop-in directory
/etc/cron.daily/            # daily jobs (run-parts)
/etc/cron.hourly/           # hourly jobs
/etc/cron.monthly/          # monthly jobs
/etc/cron.weekly/           # weekly jobs
/var/spool/cron/crontabs/   # user crontab files

Lines in /etc/crontab and /etc/cron.d/* include a username field:

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

* * * * * root /opt/scripts/check.sh
Warning

Don’t add jobs directly to /etc/crontab. Use /etc/cron.d/ instead. It’s safer across package updates.

Environment and PATH: Why Jobs Break in Cron

Cron runs commands with a minimal environment. Common failure:

# works in terminal
/opt/scripts/backup.sh

# in cron — "command not found"

Reason: cron sets PATH to only /usr/bin:/bin. Solutions:

Specify full paths explicitly:

0 3 * * * /usr/bin/python3 /opt/scripts/backup.py

Set PATH in crontab:

PATH=/usr/local/bin:/usr/bin:/bin:/opt/scripts
0 3 * * * backup.sh

Use a wrapper script:

#!/bin/bash
# /opt/scripts/run_backup.sh
source /etc/profile
cd /opt/project || exit 1
./backup.sh
Tip

Always verify variables: env | sort in terminal vs. job * * * * * env | sort > /tmp/cron_env.txt.

Output Redirection and Log Rotation

By default, cron emails stdout and stderr to the user. Set MAILTO="" to disable.

# send output to file
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# add timestamp to log (readability)
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# rotate old logs
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1 && \
  find /var/log -name "backup.log*" -mtime +7 -delete

Or use logger for syslog:

0 3 * * * /opt/scripts/backup.sh 2>&1 | logger -t backup

Timezones and TZ

Cron uses the system timezone. To override:

# option 1: variable in crontab
TZ=Europe/Moscow
0 9 * * * /opt/scripts/report.sh

# option 2: wrapper
0 9 * * * TZ=Europe/Moscow /opt/scripts/report.sh
Warning

TZ only affects the schedule. Inside the script, use $TZ explicitly: date +%Z shows system timezone.

Check schedule in UTC:

crontab -l | while read line; do
  if [[ ! "$line" =~ ^# ]] && [[ ! -z "$line" ]]; then
    echo "$line" | awk '{print $1":"$2" UTC  |  "$5" "$6" "$7" "$8" "$9" "$10}'
  fi
done

Common Pitfalls and Pre-Deployment Checks

1. Percent sign in commands

% in crontab means newline. Escape it:

# Wrong:
0 3 * * * /opt/scripts/report.sh "Report for $(date +%Y-%m-%d)"

# Right:
0 3 * * * /opt/scripts/report.sh "Report for $(date +\%Y-\%m-\%d)"

2. Overlapping jobs

If a script may run longer than the interval, use locking:

# /opt/scripts/long-task.sh
LOCKFILE=/var/run/long-task.lock

if [ -f "$LOCKFILE" ]; then
  echo "Already running" >&2
  exit 1
fi

trap "rm -f $LOCKFILE" EXIT
touch "$LOCKFILE"

# main logic
sleep 30

3. Pre-deployment checks

# show next run time for each job
for f in /etc/cron.d/*; do
  if [ -f "$f" ]; then
    echo "=== $f ==="
    head -1 "$f"
    # nearest run time
    next=$(echo "0 3 * * *" | sed 's/\*/0/g' | xargs -I{} date -d "{}" '+%Y-%m-%d %H:%M')
    echo "Next: $next"
  fi
done

# dry run
cat /etc/cron.d/my-task | grep -v "^#" | grep -v "^$" | while read schedule cmd; do
  echo "Would run: $cmd"
done

4. Syntax and validation

# check crontab format
crontab -l | grep -v "^#" | grep -v "^$" | awk '{print $1" "$2" "$3" "$4" "$5}' | \
  while read min hour dom mon dow; do
    # basic check
    echo "$min $hour $dom $mon $dow"
  done

Ansible and Cron: Idempotent Job Setup

- name: Add backup cron job
  community.general.cron:
    name: "backup database"
    minute: "0"
    hour: "3"
    job: "/opt/scripts/backup.sh >> /var/log/backup.log 2>&1"
    user: "root"
    state: present
    cron_file: "backup"
# Remove job
- name: Remove old cron job
  community.general.cron:
    name: "obsolete task"
    state: absent
    user: "root"
# Drop a file into /etc/cron.d/
- name: Deploy cron file
  ansible.builtin.copy:
    src: files/my-cron-job
    dest: /etc/cron.d/my-cron-job
    owner: root
    group: root
    mode: "0644"
  notify: restart cron

systemd Timers as an Alternative to Cron

Timers are more precise, support service dependencies, randomiseddelaysec, and calendar specs.

# /etc/systemd/system/backup.service
[Unit]
Description=Backup database

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 3am

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now backup.timer
systemctl list-timers --all | grep backup

Timer benefits:

  • dependencies (After=network.target)
  • logging via journal (journalctl -u backup.service)
  • randomiseddelaysec for staggering (avoid thundering herd)
  • one-shot and monotonic timers

Cron stays simpler for basic tasks. systemd timers are the right choice for services with dependencies and monitoring through journald.