Scheduling Jobs — Cron & systemd Timers¶
Almost every server needs to run tasks on a schedule: backups, log rotation, certificate renewals, health checks. This page covers the traditional cron daemon and the modern systemd timer, with guidance on choosing between them.
Tested on
AlmaLinux 9.4 (cronie 1.5.7, systemd 252) and Ubuntu 22.04 (cron 3.0pl1, systemd 249).
Part 1 — Cron¶
Cron is a daemon that runs commands at fixed times. On AlmaLinux/RHEL the package is cronie; on Debian/Ubuntu it is cron. It is usually installed already.
# AlmaLinux / RHEL
sudo dnf install -y cronie
sudo systemctl enable --now crond
# Debian / Ubuntu
sudo apt install -y cron
sudo systemctl enable --now cron
Editing your crontab¶
Each user has a personal crontab. Manage it with crontab:
crontab -e # edit your crontab (opens $EDITOR)
crontab -l # list your crontab
crontab -r # remove your crontab (careful — no confirmation!)
sudo crontab -e -u alice # edit another user's crontab (as root)
The five-field schedule¶
A cron line has five time fields followed by the command:
┌───────────── minute (0 - 59)
│ ┌─────────── hour (0 - 23)
│ │ ┌───────── day of month (1 - 31)
│ │ │ ┌─────── month (1 - 12)
│ │ │ │ ┌───── day of week (0 - 7, Sun = 0 or 7)
│ │ │ │ │
* * * * * command-to-run
| Field special | Meaning |
|---|---|
* |
every value |
5 |
exactly that value |
1,15,30 |
a list |
9-17 |
a range (inclusive) |
*/10 |
a step — every 10th value |
Common examples:
# Every day at 02:30
30 2 * * * /usr/local/bin/backup.sh
# Every 15 minutes
*/15 * * * * /usr/local/bin/disk-check.sh
# Weekdays at 08:00 (Mon=1 ... Fri=5)
0 8 * * 1-5 /usr/local/bin/report.sh
# First of every month at midnight
0 0 1 * * /usr/local/bin/monthly.sh
# Top of every hour
0 * * * * /usr/local/bin/hourly.sh
Cron also accepts shorthand: @reboot, @hourly, @daily, @weekly, @monthly, @yearly.
Validate before trusting
Use crontab.guru to read an unfamiliar schedule. For systemd timers there is a built-in validator — see below.
System-wide cron files¶
Beyond per-user crontabs, the system has its own:
/etc/crontab— system crontab. Lines here have an extra field: the user to run as./etc/cron.d/— drop-in files with the same format as/etc/crontab. Best place for package- or admin-managed jobs.
# /etc/cron.d/disk-check — note the user field (6th column)
SHELL=/bin/bash
PATH=/usr/sbin:/usr/bin:/sbin:/bin
*/15 * * * * root /usr/local/bin/disk-check.sh 90
The cron.daily / weekly / monthly directories¶
Drop an executable script (no schedule line, just a script) into one of these and it runs on that cadence:
sudo install -m 0755 /dev/stdin /etc/cron.daily/cleanup <<'EOF'
#!/usr/bin/env bash
find /tmp -type f -mtime +7 -delete
EOF
These are run by /etc/crontab (via run-parts) or by anacron.
Environment and PATH gotchas¶
This is the number-one cause of "works in my shell, fails in cron":
- Cron runs with a minimal environment — a short
PATH(often/usr/bin:/bin) and no profile sourced. cd, aliases, and shell functions from your.bashrcare not available.
Defensive practices:
# In the crontab, set a sane PATH at the top:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# In the script, use absolute paths and a known shell:
#!/usr/bin/env bash
/usr/bin/rsync -a /data/ /backup/data/
Capturing output and logging¶
By default cron emails any output to the job owner via local mail — which on most servers goes nowhere. Redirect explicitly:
# Append stdout AND stderr to a log file
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Discard stdout, keep errors in the log
*/5 * * * * /usr/local/bin/probe.sh >/dev/null 2>> /var/log/probe.err
You can also watch cron's own activity in the journal:
anacron — for machines that aren't always on¶
Cron assumes the machine is running at the scheduled time; a job missed while the server was off is simply skipped. anacron instead tracks when each job last ran and catches up after boot. It works in day-granularity and powers /etc/cron.daily and friends on many systems, configured via /etc/anacrontab:
# period(days) delay(min) job-id command
1 5 cron.daily run-parts /etc/cron.daily
7 25 cron.weekly run-parts /etc/cron.weekly
@monthly 45 cron.monthly run-parts /etc/cron.monthly
This is ideal for laptops and workstations that are not on 24/7.
Part 2 — systemd timers¶
systemd timers are the modern alternative. A timer is a unit that activates a matching service on a schedule. They integrate with the journal, support catch-up runs, randomized delays, and dependencies. See the systemd service management page for service-unit basics.
A timer is two units that share a name:
- A
.serviceof typeoneshotthat does the work. - A
.timerthat says when to start it.
Step 1 — the service unit¶
# /etc/systemd/system/disk-check.service
[Unit]
Description=Check disk usage and warn if over threshold
[Service]
Type=oneshot
ExecStart=/usr/local/bin/disk-check.sh 90
A oneshot service runs to completion and exits — exactly right for a scheduled job. No [Install] section is needed; the timer starts it.
Step 2 — the timer unit¶
# /etc/systemd/system/disk-check.timer
[Unit]
Description=Run disk-check every 15 minutes
[Timer]
OnCalendar=*:0/15 # every 15 minutes, on the quarter hour
Persistent=true # run a missed job after boot (like anacron)
RandomizedDelaySec=120 # spread load: start up to 120s late, at random
Unit=disk-check.service # optional; inferred from matching name
[Install]
WantedBy=timers.target
| Directive | Purpose |
|---|---|
OnCalendar= |
When to fire, in systemd calendar syntax |
Persistent=true |
If the trigger was missed (machine off), run as soon as possible |
RandomizedDelaySec= |
Jitter to avoid thundering-herd across many hosts |
OnBootSec= |
Run N seconds after boot (relative timer) |
OnUnitActiveSec= |
Run N after the unit last ran (relative timer) |
OnCalendar syntax¶
The format is DayOfWeek Year-Month-Day Hour:Minute:Second:
OnCalendar=*-*-* 02:30:00 # daily at 02:30
OnCalendar=Mon-Fri 08:00 # weekdays at 08:00
OnCalendar=*:0/15 # every 15 minutes
OnCalendar=*-*-01 00:00:00 # first of every month
OnCalendar=hourly # shorthand
OnCalendar=daily # shorthand (midnight)
Step 3 — enable and start¶
# Reload after writing/editing unit files
sudo systemctl daemon-reload
# Enable (start on boot) and start the TIMER, not the service
sudo systemctl enable --now disk-check.timer
Inspecting timers¶
# List all active timers, with last and next run times
systemctl list-timers
# Status of a specific timer and its service
systemctl status disk-check.timer
systemctl status disk-check.service
# View the job's output from the journal
journalctl -u disk-check.service --since today
$ systemctl list-timers
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sun 2026-06-07 11:45:00 IST 3min left Sun 2026-06-07 11:30:00 IST 11min ago disk-check.timer disk-check.service
Validate a calendar expression¶
systemd ships a validator — use it before deploying:
Original form: Mon-Fri 08:00
Normalized form: Mon..Fri *-*-* 08:00:00
Next elapse: Mon 2026-06-08 08:00:00 IST
(in UTC): Mon 2026-06-08 02:30:00 UTC
From now: 20h left
You can also test how a job behaves manually:
Cron vs systemd timers — which to use?¶
| Consideration | cron | systemd timer |
|---|---|---|
| Setup effort | One line — very quick | Two unit files |
| Logging | DIY redirection to a file | Automatic, in the journal |
| Missed-run catch-up | Only via anacron (daily+) | Persistent=true (any schedule) |
| Randomized jitter | No | RandomizedDelaySec= |
| Dependencies / ordering | None | Full unit dependency model |
| Resource limits, sandboxing | None | Inherits service [Service] options |
| Portability / familiarity | Universal, everyone knows it | systemd-only |
Rule of thumb
For a quick one-off or a script you'll never touch again, cron is fine. For anything you operate long-term — needs logging, catch-up, jitter, or resource control — prefer systemd timers.
Verify your work¶
# Cron: confirm the daemon is running and your job is listed
systemctl is-active crond # (or 'cron' on Debian/Ubuntu)
crontab -l
# Timer: confirm it's enabled and scheduled
systemctl is-enabled disk-check.timer # -> enabled
systemctl list-timers --all | grep disk-check
# Validate the schedule and do a manual run
systemd-analyze calendar "*:0/15"
sudo systemctl start disk-check.service
journalctl -u disk-check.service -n 20
Summary¶
- cron: edit with
crontab -e; five fieldsmin hour dom mon dow; system jobs in/etc/crontaband/etc/cron.d/; drop scripts incron.daily/weekly/monthly. - Watch for cron's minimal
PATH/environment — use absolute paths and redirect output to a log. - anacron catches up missed daily/weekly/monthly jobs on machines that aren't always on.
- systemd timers: a
oneshot.serviceplus a.timerwithOnCalendar,Persistent=true, andRandomizedDelaySec; enable the.timer, inspect withsystemctl list-timers, validate withsystemd-analyze calendar. - Use cron for quick jobs, timers for anything needing logging, catch-up, jitter, or dependencies.
Related: systemd service management · Bash scripting.