Systemd timers are the modern alternative to cron — they integrate fully with systemd, log to journald, support dependency ordering, can catch up on missed jobs, and give you much more flexible scheduling options. Every timer is paired with a service unit that does the actual work, keeping scheduling and execution cleanly separated.
Timers are not a drop-in replacement for cron — they require more setup — but for system-level scheduled tasks on modern RHEL/Ubuntu servers they are the preferred approach. Many system timers ship with packages and are already running on your system right now.
Every timer needs two unit files with the same base name:
mybackup.timer — defines WHEN to runmybackup.service — defines WHAT to run.service unit.
The service runs the job and exits. The timer waits for the next trigger.
| Type | Directive | Description |
|---|---|---|
| Realtime | OnCalendar= | Calendar-based — like cron. Fires at a specific wall-clock time. |
| Monotonic | OnBootSec=, OnActiveSec=, OnUnitActiveSec= | Relative to boot or last activation. Not affected by clock changes. |
| Persistent | Persistent=true | Catches up on missed jobs — fires immediately if timer was missed while system was off. |
# List all active timers with next/last trigger times systemctl list-timers # List ALL timers including inactive systemctl list-timers --all # Check status of a specific timer systemctl status dnf-makecache.timer systemctl status logrotate.timer # See what timers are installed on the system find /etc/systemd/system /lib/systemd/system -name "*.timer" | sortsystemctl list-timers output:
A complete example — nightly backup running at 2 AM:
# Step 1: Create the service unit (what to run) sudo tee /etc/systemd/system/nightly-backup.service << 'EOF' [Unit] Description=Nightly Backup Job After=network.target [Service] Type=oneshot User=root ExecStart=/usr/local/bin/nightly-backup.sh StandardOutput=journal StandardError=journal EOF # Step 2: Create the timer unit (when to run) sudo tee /etc/systemd/system/nightly-backup.timer << 'EOF' [Unit] Description=Run nightly backup at 2 AM Requires=nightly-backup.service [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target EOF # Step 3: Reload systemd and enable the timer sudo systemctl daemon-reload sudo systemctl enable --now nightly-backup.timer # Step 4: Verify systemctl list-timers nightly-backup.timer systemctl status nightly-backup.timer
Type=oneshot for jobs
that run and exit — like cron jobs. The service starts, runs the command,
and exits. Systemd considers it successful when the command exits 0.
OnCalendar uses systemd's calendar expression syntax — more powerful than cron's 5-field format:
# Basic calendar expressions OnCalendar=daily # every day at midnight OnCalendar=weekly # every Monday at midnight OnCalendar=monthly # first of month at midnight OnCalendar=hourly # every hour on the hour OnCalendar=minutely # every minute # Specific time OnCalendar=*-*-* 02:30:00 # every day at 2:30 AM OnCalendar=*-*-* 06,18:00:00 # 6 AM and 6 PM daily OnCalendar=Mon *-*-* 08:00:00 # every Monday at 8 AM OnCalendar=Mon-Fri *-*-* 08:00:00 # weekdays at 8 AM # Specific date OnCalendar=2026-12-31 23:59:00 # once on New Year's Eve OnCalendar=*-01-01 00:00:00 # every Jan 1st (yearly) OnCalendar=*-*-01 00:00:00 # first of every month # Intervals OnCalendar=*:0/15 # every 15 minutes OnCalendar=*:00/5 # every 5 minutes # Verify your calendar expression systemd-analyze calendar "Mon-Fri *-*-* 08:00:00" systemd-analyze calendar "*:0/15"systemd-analyze calendar output:
Monotonic timers fire relative to system events rather than wall-clock time — perfect for tasks that should run after boot or at regular intervals from startup:
# Timer that runs 5 minutes after boot sudo tee /etc/systemd/system/post-boot-check.timer << 'EOF' [Unit] Description=Run post-boot checks 5 minutes after startup [Timer] OnBootSec=5min Unit=post-boot-check.service [Install] WantedBy=timers.target EOF # Timer that runs 15 minutes after boot, then every hour sudo tee /etc/systemd/system/hourly-poll.timer << 'EOF' [Unit] Description=Poll service every hour starting 15min after boot [Timer] OnBootSec=15min OnUnitActiveSec=1h [Install] WantedBy=timers.target EOF
| Directive | Fires when... | Example |
|---|---|---|
| OnBootSec= | N time after system boot | OnBootSec=5min |
| OnStartupSec= | N time after systemd started | OnStartupSec=2min |
| OnActiveSec= | N time after timer activated | OnActiveSec=30s |
| OnUnitActiveSec= | N time after service last ran | OnUnitActiveSec=1h |
| OnUnitInactiveSec= | N time after service last finished | OnUnitInactiveSec=30min |
Persistent=true is the anacron equivalent for systemd timers —
if the system was off when the timer should have fired, it fires
immediately on next boot:
# Timer with Persistent=true — fires immediately on boot if missed
sudo tee /etc/systemd/system/weekly-report.timer << 'EOF'
[Unit]
Description=Weekly report — fires even if system was off
[Timer]
OnCalendar=Mon *-*-* 06:00:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
EOF
Persistent=true
ensures missed jobs run on next boot. RandomizedDelaySec=300
adds a random 0-300 second delay — so 50 servers don't all hammer
a shared resource at exactly the same time.
# Check when a timer last ran and whether it was missed
systemctl show nightly-backup.timer | grep -E "LastTrigger|NextElapse|Result"
# Enable and start a timer sudo systemctl enable --now nightly-backup.timer # Disable a timer (stops it running at boot) sudo systemctl disable nightly-backup.timer # Stop a running timer temporarily sudo systemctl stop nightly-backup.timer # Manually trigger the service NOW (for testing) sudo systemctl start nightly-backup.service # Check timer status systemctl status nightly-backup.timer # Check service logs journalctl -u nightly-backup.service journalctl -u nightly-backup.service -n 50 journalctl -u nightly-backup.service --since today # Check both timer and service logs together journalctl -u nightly-backup.timer -u nightly-backup.service
sudo systemctl start yourjob.service immediately
and check with journalctl -u yourjob.service.
Fix issues before the timer ever fires.
Systemd timers can run as regular users — no root required:
# Create user timer directories mkdir -p ~/.config/systemd/user/ # Create user service cat > ~/.config/systemd/user/sync-notes.service << 'EOF' [Unit] Description=Sync notes to remote [Service] Type=oneshot ExecStart=/usr/bin/rsync -av ~/notes/ user@remote:/backup/notes/ EOF # Create user timer cat > ~/.config/systemd/user/sync-notes.timer << 'EOF' [Unit] Description=Sync notes every 30 minutes [Timer] OnBootSec=5min OnUnitActiveSec=30min Persistent=true [Install] WantedBy=timers.target EOF # Enable and start (no sudo needed) systemctl --user daemon-reload systemctl --user enable --now sync-notes.timer # Check status systemctl --user list-timers systemctl --user status sync-notes.timer journalctl --user -u sync-notes.service # Enable linger so user timers survive logout loginctl enable-linger $USER
Converting existing cron jobs to systemd timers:
# OLD cron job: # 30 2 * * * root /usr/local/bin/nightly-backup.sh # NEW — nightly-backup.service cat > /etc/systemd/system/nightly-backup.service << 'EOF' [Unit] Description=Nightly Backup After=network.target [Service] Type=oneshot User=root ExecStart=/usr/local/bin/nightly-backup.sh StandardOutput=journal StandardError=journal SyslogIdentifier=nightly-backup EOF # NEW — nightly-backup.timer cat > /etc/systemd/system/nightly-backup.timer << 'EOF' [Unit] Description=Nightly Backup Timer Requires=nightly-backup.service [Timer] OnCalendar=*-*-* 02:30:00 Persistent=true [Install] WantedBy=timers.target EOF sudo systemctl daemon-reload sudo systemctl enable --now nightly-backup.timer
| Feature | Cron | Systemd Timer |
|---|---|---|
| Setup complexity | Simple — one line | Two unit files needed |
| Logging | Email or redirect to file | journald — always logged |
| Missed job recovery | No (use anacron) | Yes — Persistent=true |
| Dependencies | None | Full systemd dependency ordering |
| Resource limits | None | CPUQuota, MemoryLimit etc. |
| User timers | Per-user crontab | --user mode with linger |
| Random delay | Manual sleep in script | RandomizedDelaySec= |
| View next run | Calculate manually | systemctl list-timers |
| Command | What it does |
|---|---|
| systemctl list-timers | List active timers with next/last run |
| systemctl list-timers --all | List all timers including inactive |
| systemctl enable --now NAME.timer | Enable and start a timer |
| systemctl disable NAME.timer | Disable timer at boot |
| systemctl stop NAME.timer | Stop timer temporarily |
| systemctl start NAME.service | Manually trigger the job now |
| systemctl status NAME.timer | Timer status and next run |
| journalctl -u NAME.service | View job output logs |
| systemd-analyze calendar "expr" | Verify calendar expression |
| systemctl --user list-timers | User timers |
| loginctl enable-linger $USER | Keep user timers alive after logout |
| Expression | Meaning |
|---|---|
| daily | Every day at midnight |
| weekly | Every Monday at midnight |
| monthly | First of month at midnight |
| *-*-* 02:30:00 | Every day at 2:30 AM |
| Mon *-*-* 08:00:00 | Every Monday at 8 AM |
| Mon-Fri *-*-* 08:00:00 | Weekdays at 8 AM |
| *:0/15 | Every 15 minutes |
| *-01-01 00:00:00 | Every January 1st |