Cron and systemd Timers: Task Scheduling in Linux
Schedule recurring tasks in Linux using cron and systemd timers. Covers crontab syntax, per-user cron, system cron, and when to use systemd timers instead. Lesson 8 of the Linux & Bash Scripting course.

Two tools dominate task scheduling on Linux: cron — the decades-old standard available everywhere — and systemd timers — the modern alternative with better logging, dependency management, and missed-run handling. This lesson covers both, explains when to choose each, and shows how to build reliable scheduled jobs.
Previous: Lesson 7 — Process Management and Job Control
Cron Fundamentals
Cron reads from crontab files and runs commands at scheduled intervals. There are two types of crontabs:
| Type | Location | Owner |
|---|---|---|
| User crontab | /var/spool/cron/crontabs/<user> | Each user manages their own |
| System crontab | /etc/cron.d/, /etc/cron.daily/ etc. | Root-managed, supports username field |
# Edit your user crontab
crontab -e
# View your current crontab
crontab -l
# Edit another user's crontab (as root)
crontab -u www-data -e
# Remove your crontab
crontab -rCrontab Syntax
# ┌─────────── 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 to executeCommon Patterns
# Every minute
* * * * * /usr/local/bin/heartbeat.sh
# Every 15 minutes
*/15 * * * * /usr/local/bin/healthcheck.sh
# Daily at 2:30 AM
30 2 * * * /usr/local/bin/backup.sh
# Every Monday at midnight
0 0 * * 1 /usr/local/bin/weekly_report.sh
# First day of every month at 6 AM
0 6 1 * * /usr/local/bin/monthly_cleanup.sh
# Weekdays only at 8 AM
0 8 * * 1-5 /usr/local/bin/business_hours_task.sh
# Every 6 hours
0 */6 * * * /usr/local/bin/sync.sh
# Twice daily (8 AM and 8 PM)
0 8,20 * * * /usr/local/bin/digest.shNote: Day-of-week and day-of-month are OR-ed, not AND-ed.
0 12 1 * 1runs at noon on the 1st of each month AND every Monday — not only on Mondays that fall on the 1st.
Writing Cron-Safe Scripts
Cron jobs run with a minimal environment — no PATH beyond /usr/bin:/bin, no terminal, no home directory by default. Scripts that work interactively often fail under cron for these reasons:
#!/usr/bin/env bash
set -euo pipefail
# 1. Always set PATH explicitly
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
export PATH
# 2. Set HOME if your script needs it
HOME=/root
export HOME
# 3. Use absolute paths for all files and commands
LOG_FILE="/var/log/myapp/backup.log"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 4. Redirect all output to a log file
exec >> "$LOG_FILE" 2>&1
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup started"Crontab Entry Best Practices
# Always redirect output — cron emails stdout/stderr if mail is configured
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Or discard output if you have in-script logging
30 2 * * * /usr/local/bin/backup.sh > /dev/null 2>&1
# Use MAILTO to control cron email behaviour
MAILTO="" # Disable emails entirely
MAILTO="ops@example.com" # Send failures to this addressSystem Crontabs and /etc/cron.d/
For system-level scheduled tasks, drop a file into /etc/cron.d/ rather than root's personal crontab. System crontabs include a username field:
# /etc/cron.d/database-backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@example.com
# minute hour dom month dow user command
30 2 * * * root /usr/local/bin/pg_backup.sh production
0 3 * * * postgres /usr/local/bin/vacuum_analyze.shThe drop-in directories /etc/cron.daily/, /etc/cron.hourly/, /etc/cron.weekly/, and /etc/cron.monthly/ accept plain executable scripts — no crontab syntax required.
systemd Timers
systemd timers are the modern alternative to cron. They require two unit files: a service unit (what to run) and a timer unit (when to run it).
Advantages Over Cron
- Logging: All output captured by
journald— query withjournalctl - Missed runs:
Persistent=truecatches up if the system was off when a job was due - Dependencies: Full systemd dependency graph (
After=,Requires=) - Resource control: CPU/memory limits via
CPUQuota=,MemoryMax= - Status visibility:
systemctl list-timersshows next/last run time
Creating a systemd Timer
Step 1: Create the service unit
# /etc/systemd/system/database-backup.service
[Unit]
Description=PostgreSQL database backup
After=network.target postgresql.service
[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/pg_backup.sh production
StandardOutput=journal
StandardError=journalStep 2: Create the timer unit
# /etc/systemd/system/database-backup.timer
[Unit]
Description=Run database backup daily at 2:30 AM
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
AccuracySec=1min
[Install]
WantedBy=timers.targetStep 3: Enable and start the timer
sudo systemctl daemon-reload
sudo systemctl enable --now database-backup.timer
# Verify it's scheduled
systemctl list-timers database-backup.timerTimer Calendar Expressions
OnCalendar=daily # equivalent to *-*-* 00:00:00
OnCalendar=weekly # Monday *-*-* 00:00:00
OnCalendar=monthly # *-*-01 00:00:00
OnCalendar=hourly # *-*-* *:00:00
OnCalendar=Mon..Fri 08:00 # Weekdays at 8 AM
OnCalendar=*:0/15 # Every 15 minutes
OnCalendar=2026-*-* 03:00 # Daily in 2026 at 3 AM# Validate a calendar expression
systemd-analyze calendar "Mon..Fri 08:00"OnBootSec and OnUnitActiveSec
For relative-time timers:
[Timer]
OnBootSec=5min # Run 5 minutes after boot
OnUnitActiveSec=1h # Then repeat every hourMonitoring Scheduled Jobs
# List all active timers with next/last run info
systemctl list-timers --all
# View the journal output of a specific service
journalctl -u database-backup.service -n 50
# Follow logs in real time
journalctl -fu database-backup.service
# Check a cron job's recent execution (if using syslog)
grep CRON /var/log/syslog | tail -20
# Manually trigger a systemd service to test it
sudo systemctl start database-backup.serviceCron vs systemd Timers — When to Use Each
| Requirement | Cron | systemd Timer |
|---|---|---|
| Available on all Linux systems | Yes | Only on systemd-based systems (most modern distros) |
| Catch up missed runs | No (unless scripted) | Yes (Persistent=true) |
| Structured logging | No | Yes (journald) |
| Resource limits (CPU, memory) | No | Yes |
| Dependencies on other services | No | Yes |
| Simple schedule, no frills | Better | Overkill |
| Container/minimal environments | Often the only option | Requires systemd PID 1 |
