Scheduled Tasks: cron & timers
Scheduled Tasks: cron & timers
Every production system runs work on a schedule: log rotation, database vacuuming, certificate renewal, snapshot creation, cache warming, health reports. Two mechanisms exist on modern Linux: the classic cron daemon and systemd timers. A senior DevOps engineer must be fluent in both, know when to choose each, and understand the failure modes that bite teams in production.
cron: The Classic Scheduler
cron has been shipping on Unix systems since 1975. It is still the right tool for simple, user-owned jobs and for systems where systemd is absent. The daemon reads crontab files and runs commands at matching times.
Every crontab line follows a fixed five-field time spec followed by the command:
Managing Crontabs
Use crontab -e to edit the current user's crontab, crontab -l to list it, and crontab -r to delete it. System-wide jobs for root go in /etc/crontab or as drop-in files under /etc/cron.d/ — these have an extra field for the user to run as:
>> /var/log/myapp/job.log 2>&1 and ship those logs to your central log aggregator.
systemd Timers: The Modern Alternative
systemd timers are .timer units that activate a paired .service unit. They are more powerful than cron for production workloads because they integrate fully with journald for logging, support dependency ordering, can catch up missed runs (persistence), and emit structured events that monitoring systems can scrape.
A timer always pairs with a .service unit of the same base name. The workflow is: write the service, write the timer, enable the timer.
Persistent=true: If the system is powered off when a timer was supposed to fire, Persistent=true causes systemd to run the job immediately at next boot if the last run was missed. This is essential for nightly jobs on servers that may be rebooted for maintenance. cron provides no equivalent mechanism.
OnCalendar Syntax
systemd uses its own calendar expression language, which is more expressive than cron's five-field format. Use systemd-analyze calendar to validate any expression before deploying:
Diagram: cron vs systemd Timer Architecture
Production Scheduling Best Practices
Avoid the Thundering Herd
In a fleet of 50 servers, every cron job set to 0 3 * * * fires simultaneously. This creates a traffic spike against shared databases and APIs. For cron, add a random sleep at the start of the script: sleep $((RANDOM % 300)). For systemd timers, use RandomizedDelaySec=300 in the [Timer] block — systemd applies the jitter per host without you touching the script.
Use Locking to Prevent Overlap
If a job runs longer than its interval, a second instance starts while the first is still running. Use flock to prevent this:
For systemd services, set Type=oneshot — systemd will not start a second instance while the first is active. Combine with ExecStartPre= checks when needed.
chronic from moreutils for cron: Install moreutils and prefix cron commands with chronic. It suppresses all output unless the command exits non-zero, so cron mail only arrives when something actually fails — the opposite of the default behaviour where every run produces mail noise. Example: chronic /usr/local/bin/db-backup.sh
When to Use cron vs systemd Timers
Use cron when: you need per-user jobs (crontab -e), the target system has no systemd (containers, Alpine Linux, some embedded systems), or you are maintaining a legacy environment where consistency matters more than features.
Use systemd timers when: the job must depend on a network or database service being up (After=), you need guaranteed catch-up after a reboot (Persistent=true), you want cgroup resource limits on the job process, or you want logs to flow naturally into journald alongside your other service logs. At big-tech scale, systemd timers are the standard for host-level scheduled work because the observability and reliability story is substantially better.
CronJob resources or a distributed scheduler (Temporal, Celery Beat, AWS EventBridge). On bare-metal or VM-based hosts, systemd timers are the right tool.
Debugging Scheduled Jobs
When a job fails silently, this is the diagnostic sequence:
- Check that the timer fired:
systemctl list-timers --all | grep job-name - Check the service exit code:
systemctl status job-name.service - Read the full log:
journalctl -u job-name.service -n 100 --no-pager - Run the service manually to reproduce:
systemctl start job-name.service - For cron: check
/var/log/syslogor/var/log/cronfor the daemon's own log lines, then check whether the script uses environment variables that cron does not export (PATH, HOME — always set these explicitly at the top of cron scripts).