Skip to content
DevOps devops linux-admin 5 min read

systemd Timers

A systemd timer is a special unit file that runs another unit (almost always a service) on a schedule. Think of it as cron’s modern replacement that is built right into Ubuntu. Because timers are part of systemd (the system and service manager that boots your server and runs background services), they get proper logging, dependency tracking, and resource control for free — things plain cron simply cannot do. If you are writing automation on a modern Ubuntu 22.04 or 24.04 LTS server, timers are usually the better choice.

How timers work: the .timer + .service pair

A timer never does the actual work itself. It is a trigger. The real work lives in a separate .service unit. You always create two files that share the same base name:

  • mybackup.servicewhat to run (the command).
  • mybackup.timerwhen to run it (the schedule).

systemd automatically pairs them by name. When mybackup.timer fires, it starts mybackup.service. This separation is a feature: you can trigger the same service manually for testing without waiting for the schedule.

Unit files for system-wide tasks live in /etc/systemd/system/. Let’s build a real backup job.

Step 1: create the service

Create /etc/systemd/system/dbbackup.service:

[Unit]
Description=Back up the application database
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/dbbackup.sh
User=postgres

Type=oneshot tells systemd this is a short task that runs and exits (not a long-running daemon, which is a program that stays running in the background). User=postgres runs the script as the postgres user. The Wants/After lines mean “don’t run until the network is actually up” — a dependency cron cannot express.

Create the script and make it executable:

sudo tee /usr/local/bin/dbbackup.sh > /dev/null <<'EOF'
#!/bin/bash
set -euo pipefail
pg_dumpall | gzip > "/var/backups/db-$(date +%F).sql.gz"
EOF
sudo chmod +x /usr/local/bin/dbbackup.sh
sudo mkdir -p /var/backups

Step 2: create the timer

Create /etc/systemd/system/dbbackup.timer:

[Unit]
Description=Run the database backup daily

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true

[Install]
WantedBy=timers.target

Persistent=true is one of the best timer features: if the server was powered off at 02:30, the job runs as soon as it boots back up. Cron would just silently skip the missed run.

Step 3: enable and start the timer

After creating or editing any unit file, reload systemd so it sees your changes, then enable the timer (not the service):

sudo systemctl daemon-reload
sudo systemctl enable --now dbbackup.timer

Output:

Created symlink /etc/systemd/system/timers.target.wants/dbbackup.timer → /etc/systemd/system/dbbackup.timer.

enable makes it survive reboots; --now also starts it immediately.

OnCalendar syntax

OnCalendar defines when the timer fires. The format is DayOfWeek Year-Month-Day Hour:Minute:Second. An asterisk * means “every”, and you can use lists, ranges, and steps.

GoalOnCalendar value
Every day at 02:30*-*-* 02:30:00
Every hour, on the hour*-*-* *:00:00
Every 15 minutes*:0/15
Every Monday at 09:00Mon *-*-* 09:00:00
Weekdays at 18:00Mon..Fri *-*-* 18:00:00
First day of each month, midnight*-*-01 00:00:00
Shorthand keywordsdaily, weekly, monthly, hourly

Never guess the syntax — test it. The systemd-analyze calendar command parses an expression and shows the next few times it will fire:

systemd-analyze calendar --iterations=3 "Mon..Fri *-*-* 18:00:00"

Output:

  Original form: Mon..Fri *-*-* 18:00:00
Normalized form: Mon..Fri *-*-* 18:00:00
    Next elapse: Mon 2026-06-15 18:00:00 UTC
       (in UTC): Mon 2026-06-15 18:00:00 UTC
       From now: 7h 12min left
    Iteration 2: Tue 2026-06-16 18:00:00 UTC
    Iteration 3: Wed 2026-06-17 18:00:00 UTC

Tip: Set RandomizedDelaySpec= in the [Timer] section (for example RandomizedDelaySec=5m) to spread load. If 50 servers all run a job at exactly 02:00, they may overwhelm a shared resource. A random delay staggers them.

Listing and inspecting timers

To see every active timer, when it last ran, and when it fires next:

systemctl list-timers

Output:

NEXT                        LEFT       LAST                        PASSED    UNIT             ACTIVATES
Mon 2026-06-15 02:30:00 UTC 18h left   Sun 2026-06-14 02:30:00 UTC 5h ago    dbbackup.timer   dbbackup.service
Mon 2026-06-15 06:00:00 UTC 22h left   Sun 2026-06-14 06:00:00 UTC 1h ago    apt-daily.timer  apt-daily.service

Add --all to include inactive timers. To test the job right now without waiting, start the service directly:

sudo systemctl start dbbackup.service

This is a huge advantage over cron: there is no “comment out the schedule and wait” dance.

Reading the logs

Every run is captured by the journal (systemd’s central log store). To see output and errors from your job:

journalctl -u dbbackup.service --since today

Output:

Jun 14 02:30:00 web01 systemd[1]: Starting Back up the application database...
Jun 14 02:30:04 web01 systemd[1]: dbbackup.service: Deactivated successfully.
Jun 14 02:30:04 web01 systemd[1]: Finished Back up the application database.

With cron, output goes to local mail or /dev/null unless you set up redirection yourself. With timers, stdout and stderr are logged automatically and are searchable by unit, time, and exit status.

Timers vs cron — when to use which

Featuresystemd timercron
LoggingAutomatic, via journalctl -uManual; you must redirect output
Missed runs (downtime)Persistent=true catches upSilently skipped
DependenciesAfter=network-online.target, etc.None
Resource limitsCPUQuota=, MemoryMax= on the serviceNone
Run job manually to testsystemctl start svc.serviceAwkward
Setup effortTwo files + reloadOne crontab line
Per-user simple jobsPossible but verboseVery quick (crontab -e)

Prefer timers when: the job matters (backups, deploys, cleanup), needs reliable logging, depends on the network or another service, or must catch up after downtime.

Cron is still fine when: you need a quick one-line personal job and don’t care about logging or missed runs.

Best Practices

  • Always name the .timer and .service files with the same base name so systemd pairs them automatically.
  • Run sudo systemctl daemon-reload after every edit to a unit file, or your changes are ignored.
  • Use Type=oneshot for scheduled scripts that run and exit — never Type=simple.
  • Set Persistent=true for jobs that must not be skipped if the server was off.
  • Test the schedule with systemd-analyze calendar "..." before enabling the timer.
  • Add RandomizedDelaySec= to stagger jobs across a fleet of servers.
  • Run jobs as a dedicated low-privilege user with User= instead of root whenever possible.
Last updated June 15, 2026
Was this helpful?