Linux

Run a job on a schedule with systemd timers

Replace a cron line with a service and a timer. Every run then gets its own log and exit status, a run doesn’t start while the last one is still going, and a run the machine missed while it was off happens at the next boot.

Written againstsystemd 252 (Debian 12, RHEL 9)systemd 255 (Ubuntu 24.04)
Reading time
3 min
Commands to copy
11
Last checked
24 Oct 2026

A timer is two unit files with the same name: a .service that says what to run, and a .timer that says when. Compared with a crontab line, each run is logged in the journal with its exit code, a run doesn’t start while the previous one is still going, and Persistent=true catches up on a run the machine was switched off for.

The example dumps a PostgreSQL database every night at 02:30 and keeps 14 days of dumps.

Write the job

  1. Put the work in a script

    /usr/local/bin/backup-db.sh
    #!/bin/shset -eupg_dump --format=custom --file="/var/backups/postgres/app-$(date +%F).dump" appfind /var/backups/postgres -name '*.dump' -mtime +14 -delete
    shell
    $ sudo chmod 755 /usr/local/bin/backup-db.sh$ sudo install -d -o postgres -g postgres -m 750 /var/backups/postgres
  2. Describe it as a service

    /etc/systemd/system/backup-db.service
    [Unit]Description=Dump the app database [Service]Type=oneshotUser=postgresExecStart=/usr/local/bin/backup-db.sh

    Type=oneshot tells systemd the job is done when the command exits. The service has no [Install] section, because the timer starts it, not the boot.

  3. Run it once by hand

    shell
    $ sudo systemctl daemon-reload$ sudo systemctl start backup-db.service$ journalctl -u backup-db.service -n 20

    Fix whatever fails here before you schedule it. The command waits until the job has finished.

Schedule it with a timer

/etc/systemd/system/backup-db.timer
[Unit]Description=Dump the app database every night [Timer]OnCalendar=*-*-* 02:30:00RandomizedDelaySec=15minPersistent=true [Install]WantedBy=timers.target
SettingWhat it does
OnCalendar=When to run, in the server’s time zone. Append a zone to fix it: *-*-* 02:30:00 UTC.
RandomizedDelaySec=Waits a random time up to 15 minutes, so many machines with the same timer don’t start at once.
Persistent=trueRemembers the last run on disk. If 02:30 passed while the machine was off, the job runs right after the next boot.
WantedBy=timers.targetStarts the timer at boot once it’s enabled.
shell
$ sudo systemctl daemon-reload$ sudo systemctl enable --now backup-db.timer

Check when it runs

systemd-analyze calendar shows how systemd reads an expression and when it fires next. Use it before you trust a new schedule.

shell
$ systemd-analyze calendar 'Mon..Fri 07:00'  Original form: Mon..Fri 07:00Normalized form: Mon..Fri *-*-* 07:00:00    Next elapse: Tue 2026-11-03 07:00:00 CET       (in UTC): Tue 2026-11-03 06:00:00 UTC       From now: 13h left
OnCalendar=Runs
hourlyAt minute 0 of every hour
dailyAt midnight
*:0/15Every 15 minutes, at :00, :15, :30 and :45
Mon..Fri 07:00On weekdays at 07:00
*-*-01 04:00On the first of every month at 04:00
Sun *-*-1..7 03:00On the first Sunday of the month at 03:00

Every timer on the machine, with its last and next run:

shell
$ systemctl list-timersNEXT                        LEFT    LAST                        PASSED  UNIT             ACTIVATESTue 2026-11-03 02:41:12 CET 9h left Mon 2026-11-02 02:37:55 CET 15h ago backup-db.timer  backup-db.service

Read what a run did

shell
$ systemctl status backup-db.service○ backup-db.service - Dump the app database     Loaded: loaded (/etc/systemd/system/backup-db.service; static)     Active: inactive (dead) since Mon 2026-11-02 02:38:41 CET; 15h agoTriggeredBy: ● backup-db.timer    Process: 48122 ExecStart=/usr/local/bin/backup-db.sh (code=exited, status=0/SUCCESS)   Main PID: 48122 (code=exited, status=0/SUCCESS)        CPU: 41.233s$ journalctl -u backup-db.service --since yesterday

A run that exits with anything but 0 leaves the service in the failed state, and systemctl --failed lists it until the next run succeeds.

Move a crontab line over

crontabOnCalendar=
30 2 * * **-*-* 02:30:00
*/15 * * * **:0/15
0 7 * * 1-5Mon..Fri 07:00
0 4 1 * **-*-01 04:00
@rebootNo calendar: use OnBootSec=1min in the timer

Cron runs a job with a minimal environment and mails its output to the user. A service runs with systemd’s environment instead, and its output goes to the journal. If the script relied on variables from a login shell, set them with Environment= in the [Service] section. Remove the crontab line after the timer’s first successful run, so the job doesn’t run twice.

Last checked on 24 October 2026 against systemd 252 (Debian 12, RHEL 9) and systemd 255 (Ubuntu 24.04). If a step doesn’t match what you see, contact us with the command and what it printed.