LinuxBash Scripting

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.

TT
Daniel Brooks
6 min read
Cron and systemd Timers: Task Scheduling in Linux

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:

TypeLocationOwner
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
bash
# 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 -r

Crontab Syntax

text
# ┌─────────── 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 execute

Common Patterns

text
# 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.sh

Note: Day-of-week and day-of-month are OR-ed, not AND-ed. 0 12 1 * 1 runs 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:

bash
#!/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

text
# 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 address

System 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:

text
# /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.sh

The 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 with journalctl
  • Missed runs: Persistent=true catches 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-timers shows next/last run time

Creating a systemd Timer

Step 1: Create the service unit

ini
# /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=journal

Step 2: Create the timer unit

ini
# /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.target

Step 3: Enable and start the timer

bash
sudo systemctl daemon-reload
sudo systemctl enable --now database-backup.timer

# Verify it's scheduled
systemctl list-timers database-backup.timer

Timer Calendar Expressions

text
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
bash
# Validate a calendar expression
systemd-analyze calendar "Mon..Fri 08:00"

OnBootSec and OnUnitActiveSec

For relative-time timers:

ini
[Timer]
OnBootSec=5min          # Run 5 minutes after boot
OnUnitActiveSec=1h      # Then repeat every hour

Monitoring Scheduled Jobs

bash
# 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.service

Cron vs systemd Timers — When to Use Each

RequirementCronsystemd Timer
Available on all Linux systemsYesOnly on systemd-based systems (most modern distros)
Catch up missed runsNo (unless scripted)Yes (Persistent=true)
Structured loggingNoYes (journald)
Resource limits (CPU, memory)NoYes
Dependencies on other servicesNoYes
Simple schedule, no frillsBetterOverkill
Container/minimal environmentsOften the only optionRequires systemd PID 1