| name | cron-scheduling |
| description | Schedule and manage recurring tasks with cron and systemd timers. Use when setting up cron jobs, writing systemd timer units, handling timezone-aware scheduling, monitoring failed jobs, implementing retry patterns, or debugging why a scheduled task didn't run. |
| metadata | {"clawdbot":{"emoji":"⏰","requires":{"anyBins":["crontab","systemctl","at"]},"os":["linux","darwin"]}} |
Cron & Scheduling
Schedule and manage recurring tasks. Covers cron syntax, crontab management, systemd timers, one-off scheduling, timezone handling, monitoring, and common failure patterns.
When to Use
- Running scripts on a schedule (backups, reports, cleanup)
- Setting up systemd timers (modern cron alternative)
- Debugging why a scheduled job didn't run
- Handling timezones in scheduled tasks
- Monitoring and alerting on job failures
- Running one-off delayed commands
Cron Syntax
The five fields
┌───────── minute (0-59)
│ ┌─────── hour (0-23)
│ │ ┌───── day of month (1-31)
│ │ │ ┌─── month (1-12 or JAN-DEC)
│ │ │ │ ┌─ day of week (0-7, 0 and 7 = Sunday, or SUN-SAT)
│ │ │ │ │
* * * * * command
Common schedules
* * * * * /path/to/script.sh
*/5 * * * * /path/to/script.sh
0 * * * * /path/to/script.sh
30 2 * * * /path/to/script.sh
0 9 * * 1 /path/to/script.sh
0 8 * * 1-5 /path/to/script.sh
0 0 1 * * /path/to/script.sh
*/15 9-17 * * 1-5 /path/to/script.sh
0 9,17 * * * /path/to/script.sh
0 0 1 1,4,7,10 * /path/to/script.sh
0 3 * * 0 /path/to/script.sh
Special strings (shorthand)
@reboot /path/to/script.sh
@yearly /path/to/script.sh
@monthly /path/to/script.sh
@weekly /path/to/script.sh
@daily /path/to/script.sh
@hourly /path/to/script.sh
Crontab Management
crontab -e
crontab -l
sudo crontab -u www-data -e
crontab -r
crontab mycrontab.txt
crontab -l > crontab-backup-$(date +%Y%m%d).txt
Crontab best practices
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=admin@example.com
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=admin@example.com
SHELL=/bin/bash
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
0 3 * * 0 find /var/log/myapp -name "*.log" -mtime +30 -delete
*/5 * * * * /opt/scripts/healthcheck.sh || /opt/scripts/alert.sh "Health check failed"
Systemd Timers
Create a timer (modern cron replacement)
[Unit]
Description=Daily backup
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=backup
StandardOutput=journal
StandardError=journal
[Unit]
Description=Run backup daily at 2 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers
systemctl list-timers --all
systemctl status backup.service
journalctl -u backup.service --since today
sudo systemctl start backup.service
sudo systemctl disable --now backup.timer
OnCalendar syntax
OnCalendar=daily
OnCalendar=Mon *-*-* 09:00:00
OnCalendar=*:0/15
OnCalendar=Mon..Fri *-*-* 08:00:00
OnCalendar=*-*-01 00:00:00
OnCalendar=0/6:00:00
OnCalendar=2026-02-03 12:00:00
systemd-analyze calendar "Mon *-*-* 09:00:00"
systemd-analyze calendar "*:0/15"
systemd-analyze calendar --iterations=5 "Mon..Fri *-*-* 08:00:00"
Advantages over cron
Systemd timers vs cron:
+ Logs in journald (journalctl -u service-name)
+ Persistent: catches up on missed runs after reboot
+ RandomizedDelaySec: prevents thundering herd
+ Dependencies: can depend on network, mounts, etc.
+ Resource limits: CPUQuota, MemoryMax, etc.
+ No lost-email problem (MAILTO often misconfigured)
- More files to create (service + timer)
- More verbose configuration
One-Off Scheduling
at (run once at a specific time)
echo "/opt/scripts/deploy.sh" | at 2:00 AM tomorrow
echo "reboot" | at now + 30 minutes
echo "/opt/scripts/report.sh" | at 5:00 PM Friday
at 10:00 AM
> /opt/scripts/task.sh
> echo "Done" | mail -s "Task complete" admin@example.com
> <Ctrl+D>
atq
at -c <job-number>
atrm <job-number>
sleep-based (simplest)
(sleep 3600 && /opt/scripts/task.sh) &
nohup bash -c "sleep 7200 && /opt/scripts/task.sh" &
Timezone Handling
timedatectl
date +%Z
TZ=America/New_York
0 9 * * * /opt/scripts/report.sh
export TZ=UTC
TZ=Europe/London date '+%Y-%m-%d %H:%M:%S'
timedatectl list-timezones
timedatectl list-timezones | grep America
DST pitfalls
Problem: A job scheduled for 2:30 AM may run twice or not at all
during DST transitions.
"Spring forward": 2:30 AM doesn't exist (clock jumps 2:00 → 3:00)
"Fall back": 2:30 AM happens twice
Mitigation:
1. Schedule critical jobs outside 1:00-3:00 AM
2. Use UTC for the schedule: TZ=UTC in crontab
3. Make jobs idempotent (safe to run twice)
4. Systemd timers handle DST correctly
Monitoring and Debugging
Why didn't my cron job run?
systemctl status cron
systemctl status crond
grep CRON /var/log/syslog
grep CRON /var/log/cron
journalctl -u cron --since today
crontab -l
env -i HOME=$HOME SHELL=/bin/sh PATH=/usr/bin:/bin /opt/scripts/backup.sh
ls -la /opt/scripts/backup.sh
ls -la /var/spool/cron/
Job wrapper with logging and alerting
#!/bin/bash
set -euo pipefail
JOB_NAME="${1:?Usage: cron-wrapper.sh <job-name> <command> [args...]}"
shift
COMMAND=("$@")
LOG_DIR="/var/log/cron-jobs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/$JOB_NAME.log"
log() { echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*" >> "$LOG_FILE"; }
log "START: ${COMMAND[*]}"
START_TIME=$(date +%s)
if "${COMMAND[@]}" >> "$LOG_FILE" 2>&1; then
ELAPSED=$(( $(date +%s) - START_TIME ))
log "SUCCESS (${ELAPSED}s)"
else
EXIT_CODE=$?
ELAPSED=$(( $(date +%s) - START_TIME ))
log "FAILED with exit code $EXIT_CODE (${ELAPSED}s)"
echo "Cron job '$JOB_NAME' failed with exit $EXIT_CODE" | \
mail -s "CRON FAIL: $JOB_NAME" admin@example.com 2>/dev/null || true
0 2 * * * /opt/scripts/cron-wrapper.sh daily-backup /opt/scripts/backup.sh
*/5 * * * * /opt/scripts/cron-wrapper.sh health-check /opt/scripts/healthcheck.sh
Lock to prevent overlap
* * * * * flock -n /tmp/myjob.lock /opt/scripts/slow-job.sh
LOCKFILE="/tmp/myjob.lock"
exec 200>"$LOCKFILE"
flock -n 200 || { echo "Already running"; exit 0; }
Idempotent Job Patterns
BACKUP_DIR="/backups/$(date +%Y%m%d)"
[[ -d "$BACKUP_DIR" ]] && { echo "Backup already exists"; exit 0; }
mkdir -p "$BACKUP_DIR"
pg_dump mydb > "$BACKUP_DIR/mydb.sql"
find /tmp/uploads -mtime +7 -type f -delete 2>/dev/null || true
rsync -az /data/ backup-server:/backups/data/
Tips
- Always redirect output in cron jobs:
>> /var/log/job.log 2>&1. Without this, output goes to mail (if configured) or is silently lost.
- Test cron jobs by running them with
env -i to simulate cron's minimal environment. Most failures are caused by missing PATH or environment variables.
- Use
flock to prevent overlapping runs when a job might take longer than its schedule interval.
- Make all scheduled jobs idempotent. If a job runs twice (DST, manual trigger, crash recovery), it should produce the same result.
systemd-analyze calendar is invaluable for verifying timer schedules before deploying.
- Never schedule critical jobs between 1:00 AM and 3:00 AM if DST applies. Use UTC schedules instead.
- Log the start time, end time, and exit code of every cron job. Without this, debugging failures after the fact is guesswork.
- Prefer systemd timers over cron for production services: you get journald logging, missed-run catchup (
Persistent=true), and resource limits for free.