4. Cron and task scheduling โ
๐ฏ What you'll learn in this chapter:
cronbasics โcrontab, the 5-field syntax- The PATH problem โ bash's most common cron trap
atโ a one-time delayed tasksystemdtimers โ the modern alternativeflockโ the production pattern for a single-instance guarantee- A real example โ Nightly backup with notifications
โฑ Time: ~25 minutes ๐งช Exercises:
bashlings watchโ 7 interactive exercises ready (exercises/14_cron/)
4.1. Why do you need scheduling? โ
A server's daily automated jobs:
| Task | Typical time | Tool |
|---|---|---|
| Database backup | every night | cron / systemd |
| Log rotation | every day | logrotate (cron) |
| SSL certificate renewal | twice a month | certbot (cron) |
| Cache cleanup | every 15 minutes | cron |
| Health check & alert | every 5 minutes | cron / monitor |
| Index rebuild | on weekends | cron |
Core idea
Any task that runs reliably and continuously without human involvement is a scheduled task. The mantra: "If you do it more than twice a month โ automate it."
4.2. cron basics โ
cron โ the classic scheduler of Unix systems (since 1975).
Core commands โ
crontab -l # current list
crontab -e # edit (opens the default editor)
crontab -r # โ delete EVERYTHING โ careful!
crontab my.cron # import from a file
crontab -u ali -l # another user's cron (as root)Locations โ
~/ โ user cron, via `crontab -e`
/var/spool/cron/<user> โ crons are stored here (root only)
/etc/crontab โ system-wide cron (with an extra user field)
/etc/cron.d/ โ additional cron files
/etc/cron.daily/ โ scripts run every day
/etc/cron.hourly/
/etc/cron.weekly/
/etc/cron.monthly/crontab -r โ careful!
The -r flag deletes your entire cron table without asking. There is no -i flag!
Make a backup:
crontab -l > ~/cron-backup-$(date +%F).txt4.3. Cron syntax โ the 5 fields โ
* * * * * command
โ โ โ โ โ
โ โ โ โ โโโ day of week (0-7, 0 and 7 = Sunday)
โ โ โ โโโโโโ month (1-12)
โ โ โโโโโโโโโ day (1-31)
โ โโโโโโโโโโโโ hour (0-23)
โโโโโโโโโโโโโโโ minute (0-59)Special characters โ
| Character | Meaning | Example |
|---|---|---|
* | every value | * * * * * โ every minute |
, | list | 0,15,30,45 * * * * โ once every 15 minutes |
- | range | 9-17 โ hours 9 through 17 |
/ | step | */5 โ every 5 minutes |
*/N | with a step | */15 * * * * |
Real examples โ
# Every minute
* * * * * /opt/heartbeat.sh
# Every 5 minutes
*/5 * * * * /opt/healthcheck.sh
# At minute 0 of every hour
0 * * * * /opt/log-rotate.sh
# Every day at 02:00
0 2 * * * /opt/backup.sh
# Every Sunday night at 03:30
30 3 * * 0 /opt/weekly-report.sh
# Every hour during business hours 9-17 (1-5 = Monday-Friday)
0 9-17 * * 1-5 /opt/business-check.sh
# On the 1st of every month at 00:30
30 0 1 * * /opt/monthly-billing.sh
# Every 3 hours
0 */3 * * * /opt/cache-refresh.sh
# Every 30 minutes, on weekdays only
*/30 * * * 1-5 /opt/sync.shSpecial strings โ @ โ
| String | Equivalent | Meaning |
|---|---|---|
@reboot | (on boot) | Once when the system boots |
@yearly | 0 0 1 1 * | Once a year |
@monthly | 0 0 1 * * | Once a month |
@weekly | 0 0 * * 0 | Once a week (Sunday) |
@daily | 0 0 * * * | Once a day |
@hourly | 0 * * * * | Once an hour |
@reboot /opt/startup.sh
@daily /opt/backup.sh
@hourly /opt/cleanup.shCrontab generator
For complex syntax โ crontab.guru โ interactive online analysis. A reliable tool.
4.4. Cron environment and the PATH problem โ
The most common cron error: "The script works in the terminal but not in cron."
The reason โ in the cron environment the PATH is minimal:
# Looks simple, but wrong:
* * * * * mybackup.sh
# cron: `mybackup` not found (not in PATH)Cron PATH default โ
On most systems:
PATH=/usr/bin:/binMissing: /usr/local/bin, ~/bin, ~/.cargo/bin, ~/.local/bin, etc.
Solution โ 3 ways โ
1. Use the full path (the most precise):
* * * * * /usr/local/bin/mybackup.sh2. Set PATH in the cron file:
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
* * * * * mybackup.sh3. Load PATH at the start of the script:
#!/usr/bin/env bash
source ~/.bashrc # or explicitly:
export PATH="/usr/local/bin:/usr/bin:/bin"
# ... work ...Login shell vs Cron shell
Cron is a non-interactive shell. If ~/.bashrc has [[ $- == *i* ]] && return, the settings won't be loaded.
To test:
* * * * * env > /tmp/cron-env.txtWait one minute, then run cat /tmp/cron-env.txt โ you'll see the cron environment.
4.5. Logging and debugging โ
By default, cron output is sent to the user via mail. If mail isn't configured โ it's lost.
The good way โ always redirect to a file โ
# Both stdout and stderr to one file
0 2 * * * /opt/backup.sh >> /var/log/backup.log 2>&1
# stdout and stderr separately
0 2 * * * /opt/backup.sh > /var/log/backup.out 2>> /var/log/backup.err
# Discard everything (if you don't want it at all)
0 2 * * * /opt/silent.sh > /dev/null 2>&1Disabling MAILTO
To get rid of the mail nuisance, at the top of the crontab:
MAILTO=""
0 2 * * * /opt/backup.sh >> /var/log/backup.log 2>&1Or to a specific email:
MAILTO="admin@example.com"System cron logs โ
# Ubuntu / Debian
tail -f /var/log/syslog | grep CRON
# RHEL / CentOS
tail -f /var/log/cron
# macOS
log show --predicate 'process == "cron"' --last 1hScript debug mode โ
#!/usr/bin/env bash
set -euo pipefail
# When running in cron, write to the log
exec >> /var/log/myscript.log 2>&1
echo "===== $(date) ====="
# ... main work ...exec >> file 2>&1 โ redirects all subsequent output of the script to the file.
4.6. at โ a one-time delayed task โ
cron is recurring. at runs once at a future time.
# Today at 18:00
echo "/opt/notify.sh" | at 18:00
# Tomorrow
echo "/opt/task.sh" | at 09:00 tomorrow
# In 1 hour
echo "/opt/reminder.sh" | at now + 1 hour
# A specific date
echo "/opt/event.sh" | at 14:30 2026-06-01
# Interactive (multiple lines)
at 22:00
> notify-send "Time's up"
> /opt/cleanup.sh
> Ctrl+DManagement โ
atq # pending tasks
# 5 Sat May 17 18:00:00 2026 a ali
atrm 5 # cancel
at -c 5 # view the commands inside itCron vs at โ when to use which
- Cron โ recurring: backup, monitoring, cleanup
atโ one-time: "reboot the server tomorrow morning"
4.7. systemd timers โ the modern alternative โ
Modern Linux distributions have systemd โ with many advantages over cron.
Structure โ .service + .timer โ
/etc/systemd/system/backup.service:
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/opt/backup.sh
StandardOutput=journal
StandardError=journal/etc/systemd/system/backup.timer:
[Unit]
Description=Nightly backup timer
Requires=backup.service
[Timer]
OnCalendar=daily # = 00:00
OnCalendar=*-*-* 02:00:00 # exactly 02:00
Persistent=true # catch up missed runs
RandomizedDelaySec=30m # random delay of 0-30 minutes
[Install]
WantedBy=timers.targetEnabling and managing โ
# Reload (after adding a new file)
sudo systemctl daemon-reload
# Enable and start
sudo systemctl enable --now backup.timer
# Status
systemctl status backup.timer
systemctl list-timers --all
# Logs
journalctl -u backup.service # service log
journalctl -u backup.service --since "1 hour ago"
# Manual run (test)
sudo systemctl start backup.serviceOnCalendar= syntax โ
OnCalendar=hourly # every hour
OnCalendar=daily # every day at 00:00
OnCalendar=weekly # every Sunday
OnCalendar=monthly # on the 1st of every month
OnCalendar=Mon..Fri 09:00 # weekdays at 9:00
OnCalendar=*-*-* 02:00:00 # every day at 02:00
OnCalendar=*:0/15 # every 15 minutes
OnCalendar=Sat,Sun 04:00 # on weekendssystemd-analyze calendar
systemd-analyze calendar "Mon..Fri 09:00"
# Original form: Mon..Fri 09:00
# Normalized form: Mon..Fri *-*-* 09:00:00
# Next elapse: Mon 2026-05-19 09:00:00 +05Check that your expression is correct first.
4.8. cron vs systemd timers โ a comparison โ
| Feature | cron | systemd timer |
|---|---|---|
| Age | 1975 | 2010 |
| Universal Unix | โ | โ (only Linux with systemd) |
| macOS / *BSD | โ | โ |
| Logging integration | manual | โ journalctl |
| Catching up missed runs | โ | โ
Persistent=true |
| Sandboxing (security) | โ | โ full |
Resource limits (MemoryMax) | โ | โ |
| Dependencies (on other services) | โ | โ
Requires= |
| Simple cases | โ | overkill |
| Complex workflows | hard | โ natural |
Rule
- A single server, a simple task โ
cron - Production services โ
systemdtimers - macOS โ
cron(orlaunchd)
macOS โ launchd
On macOS, cron is officially deprecated (but still works). The recommended tool is launchd (.plist files in ~/Library/LaunchAgents/).
For simple cases, you can use cron. But on macOS cron sometimes won't work without Full Disk Access permission โ you may need to grant it via System Preferences.
4.9. flock โ a single-instance guarantee โ
The most classic cron problem: a script is supposed to run in 1 minute, but it's taking 2 minutes. On the next tick it starts again โ two instances in parallel. A race over the disk or database.
Solution: a lock file with flock.
* * * * * flock -n /tmp/myjob.lock /opt/myjob.shflock -n โ if it can't acquire the lock, it exits immediately (with an error). The second instance won't start.
Inside the script โ
#!/usr/bin/env bash
exec 200>/var/run/myjob.lock
flock -n 200 || { echo "Already running"; exit 1; }
# ... main work ...flock flags โ
| Flag | Meaning |
|---|---|
-n | Exit immediately if the lock can't be acquired |
-w <sec> | Wait N seconds, then return an error |
-x | Exclusive lock (default) |
-s | Shared lock |
-u | Unlock (manually) |
# Wait 30s, then if it errors, write it to the log
* * * * * flock -w 30 /tmp/myjob.lock /opt/myjob.sh >> /var/log/myjob.log 2>&14.10. A real example โ a production-grade nightly backup โ
#!/usr/bin/env bash
#
# nightly-backup.sh โ production backup with notifications
#
# crontab entry:
# 0 2 * * * flock -n /tmp/backup.lock /opt/bin/nightly-backup.sh
#
set -euo pipefail
IFS=$'\n\t'
# === Configuration ===
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_FILE="/var/log/backup.log"
readonly SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"
readonly HEALTHCHECK_URL="${HEALTHCHECK_URL:-}"
readonly BACKUP_DIR="/var/backups"
readonly SOURCE_DIRS=("/var/www" "/etc")
readonly KEEP_DAYS=14
# === Logging ===
exec >> "$LOG_FILE" 2>&1
echo ""
echo "===== $(date '+%Y-%m-%d %H:%M:%S') ====="
log() { printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*"; }
# === Notification ===
notify_slack() {
[[ -z "$SLACK_WEBHOOK" ]] && return 0
local msg="$1"
curl -fsS -X POST -H 'Content-Type: application/json' \
-d "{\"text\":\"$msg\"}" \
"$SLACK_WEBHOOK" > /dev/null
}
# === Cleanup and error handling ===
cleanup() {
local rc=$?
if [[ $rc -ne 0 ]]; then
log "โ Backup ended with an error (exit=$rc)"
notify_slack ":x: Backup failed on $(hostname) (exit=$rc, see $LOG_FILE)"
fi
}
trap cleanup EXIT
# === Main work ===
log "๐ฆ Backup started"
mkdir -p "$BACKUP_DIR"
archive="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).tar.gz"
log "๐ Archive: $archive"
if ! tar -czf "$archive" "${SOURCE_DIRS[@]}" 2>&1; then
log "โ tar failed"
exit 1
fi
size=$(du -sh "$archive" | cut -f1)
log "โ
Archive ready: $size"
# === Cleaning up old backups ===
log "๐งน Deleting backups older than ${KEEP_DAYS} days..."
find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime "+$KEEP_DAYS" -delete
# === Healthcheck ping ===
if [[ -n "$HEALTHCHECK_URL" ]]; then
curl -fsS --max-time 10 "$HEALTHCHECK_URL" > /dev/null \
&& log "๐ Healthcheck ping sent"
fi
# === Success notification ===
notify_slack ":white_check_mark: Backup OK on $(hostname) โ size: $size"
log "๐ Finished"Crontab entry โ
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
SLACK_WEBHOOK=https://hooks.slack.com/services/...
HEALTHCHECK_URL=https://hc-ping.com/uuid-here
# Every day at 02:00 โ with a lock, single instance
0 2 * * * flock -n /tmp/backup.lock /opt/bin/nightly-backup.shWhat does this script do? โ
| Feature | Where |
|---|---|
| Single instance with a lock | flock -n in crontab |
| A central log file | exec >> "$LOG_FILE" 2>&1 |
| Slack notification (success+fail) | notify_slack |
| Healthcheck.io ping | curl HEALTHCHECK_URL |
| Auto-cleanup of old backups | find ... -mtime +N -delete |
| Error handling via trap | trap cleanup EXIT |
| Production-grade error reporting | exit code โ Slack alert |
4.11. Common mistakes โ
Classic traps
The PATH problem. The script works in the terminal but not in cron. Solution: the full path, or
PATH=...at the top of the crontab.Output is lost. By default, cron output goes to mail. Solution:
>> file 2>&1always.MAILTOnot configured, mail spam every minute.MAILTO=""at the top of the crontab.~doesn't work in cron.~is for an interactive shell. In cron, use$HOMEor the full path.Confusing
crontab -r.-r= remove (everything).-e= edit. If you delete by accident โ you've lost it.Time zone โ UTC vs local. The server is usually in UTC. On a new server, check with
timedatectl status.The
%character is special in cron. In a cron line,%means a newline. If you need a literal character, escape it:\%. Better โ movedate +"%F"into a separate script.No single-instance guarantee. If you don't use
flockโ a long-running job can run twice in parallel.macOS cron Full Disk Access. System Preferences โ Security & Privacy โ Full Disk Access โ add
cron.Not understanding how a faulty cron file update behaves. When you save via
crontab -eโ it takes effect immediately. But no SIGTERM is sent: a running task keeps using the old syntax.
4.12. Exercises โ
๐งช Bashlings โ interactive exercises
This chapter's 7 exercises come with auto-checking via the bashlings CLI. None require a cron daemon โ they work with syntax and parsing:
bashlings watch # start from the first pending exercise
bashlings run cron1 # check a single exercise
bashlings hint cron1 # step-by-step hintSource: exercises/14_cron/
Also try the following additional conceptual exercises:
Cron syntax parser โ a quiz on explaining the following cron lines as "minute 30 of every hour" or "every 30 minutes during business hours 9-17":
30 * * * **/30 9-17 * * 1-50 0 1 * *
Logging wrap โ convert the following cron line into the correct form with logging + MAILTO:
cron* * * * * mybackup.shflocklock โ addflock -nto the cron line for themyjob.shscript, with the lock file being/tmp/myjob.lock.atreminder โ create anatjob that prints "time's up" after 30 minutes and check it withatq.Systemd timer โ write a
myping.serviceandmyping.timerfile that pings every 15 minutes.
4.13. Summary โ
| Concept | Key point |
|---|---|
crontab -e | Edit |
crontab -l | Current list |
crontab -r | โ Delete EVERYTHING |
| 5 fields | minute hour day month day-of-week |
*/5 | Every 5 (step) |
@daily, @hourly | Special strings |
0 2 * * * | Every day at 02:00 |
>> log 2>&1 | Output always to a file |
MAILTO="" | Turn off mail spam |
PATH=... | Mandatory to set in cron |
flock -n /tmp/x.lock | Single-instance guarantee |
at 18:00 | A one-time delay |
OnCalendar=daily | The systemd timer equivalent |
journalctl -u <service> | systemd logs |
5 key ideas โ
- PATH is minimal in cron โ use the full path or
PATH=...at the top of the crontab. >> file 2>&1always โ otherwise output is lost.flock -nโ on every production cron line.crontab.guruโ check complex syntax online.systemd timerโ preferred for production on Linux (logging + sandboxing + missed-run catch-up).
๐ Now you can automate tasks. In the next chapter โ working with containers via Docker.
Next page: 5. Integration with Docker โ