SYSTEMD · 2026-11-03

systemd restart service daily — stop cron-scripting it

Restart a systemd service daily with a timer, not a cron line: OnCalendar=daily, RandomizedDelaySec, Persistent. Bench-fired on systemd 262 — receipts inside.

HUMANby @mrkeepalive1h6 min read

The timer is the built-in way to restart a systemd service on a schedule. OnCalendar=daily picks midnight, RandomizedDelaySec spreads the load past 00:00, Persistent=true catches up after downtime. Cron can run the same restart — but blind, at exactly midnight, alongside every other job on the box, with no record of when it last fired. On tonight's bench the timer fired three times in ninety-five seconds and the journal named each one to the tenth of a second.

What does the daily-restart recipe look like?

Two files. One knows a single trick — restart the app. The other decides when. Neither touches the app's own unit.

# /etc/systemd/system/app-restart.service
[Unit]
Description=scheduled restart of app

[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl try-restart app.service

# /etc/systemd/system/app-restart.timer
[Unit]
Description=restart app once a day

[Timer]
OnCalendar=daily
RandomizedDelaySec=45m
Persistent=true

[Install]
WantedBy=timers.target

verify: sudo systemctl daemon-reload && sudo systemctl enable --now app-restart.timer && systemctl list-timers app-restart.timer — a populated NEXT column means the timer owns the schedule now.

try-restart, not restart: if the app happens to be down, plain restart would silently start it — maybe resurrecting something you stopped on purpose. try-restart only touches what is already running. Services that must never stay down swap in restart and sleep fine.

OnCalendar=dailythe calendar wheel · 00:00 RandomizedDelaySecup to 45 min, drawn per fire Persistent=truethe stamp · missed days fire on boot oneshot · try-restartonly if the app is running app back, freshsame unit, new PID
Five moving parts, one daily restart. The stamp is the only state on disk — a zero-byte file.

Why not just use cron?

Because the cron crutch works once and then leans on you. @daily systemctl restart app is one line in root's crontab — and then the questions start. When did it last fire? Did it fire at all while the box was off? Why did every job on the machine stamp 00:00:00 together? Cron's man page defines @daily as midnight sharp; every service scheduled that way hits the same turnstile at the same second, and the first one through is the disk.

The honest ledger: cron restarts the app, and that is all it does. No list-timers column, no stamp on disk, no catch-up after downtime, no journal unit to read — if cron's own daemon is even installed. Our bench VM's base image ships none. On a modern Arch box the timers are already there under PID 1, waiting.

the questioncron @dailythe timer
When did it last fire?grep logs, maybelist-timers, LAST column
Box was off at midnight?skipped, silentlyPersistent fires on boot
Exactly when does it run?00:00:00 sharp00:00 plus the drawn delay
Extra daemon to run?cron or crondnone — PID 1 already does it
Where are the logs?mailed, if at alljournalctl -u, structured

The general case — when a timer beats a cron line and why — lives in our earlier piece, Cron vs systemd timers: cron runs lines, timers run units. This one is just the nightly-restart special case.

How do I stop the midnight stampede?

One line: RandomizedDelaySec=45m. After each calendar elapse the timer draws a fresh random delay, so a dozen nightly restarts land anywhere inside the window instead of one instant. The bench ran a minute-sized version of the recipe — calendar at :30 past, delay capped at 20 seconds — and every draw landed somewhere else:

bench receipts, unedited — systemd 262 on Arch
Nov 03 05:13:48.974739 bench-arch systemd[1]: Starting scheduled restart of app...
RandomizedDelayUSec=20s
LastTriggerUSec=Tue 2026-11-03 05:14:40 UTC
NextElapseUSecRealtime=Tue 2026-11-03 05:15:43 UTC

Read the arithmetic off the lines: calendar 30, cap 20 — the draws came out at 18.97, 10.42 and 13 seconds past. Three fires, three different seconds, all inside the cap. Cron cannot draw; its man page schedules to the fixed second, forever. Scale back up to 45m and your overnight restarts spread across the window, and the disk never meets a crowd.

What if the box was off at restart time?

Persistent=true is the catch-up clause. After every fire the timer touches a zero-byte stamp — /var/lib/systemd/timers/stamp-app-restart.timer — and boot-time systemd compares it to the calendar. Box off all night, boot at 09:00: the missed restart runs immediately, once. Without the line, the missed fire skips silently and the app waits a full day for its reset. The stamp is a zero-byte promise: I owe you one restart after the next boot.

the stamp directory on the bench, unedited
-rw-r--r-- 1 root root 0 Nov  3 05:13 stamp-app-restart.timer
-rw-r--r-- 1 root root 0 Oct 24 10:42 stamp-archlinux-keyring-wkd-sync.timer

The second line is not ours: the base image ships with that timer already enabled and stamped. The pattern is the default scheduler's own habit — Arch's keyring updates catch up after downtime the same way your app will.

Isn't Restart=always the same thing?

No, and mixing them up is how midnight outages get invented. Restart=always is failure-driven: the main process exits, systemd starts it again after RestartSec. While the app runs healthy, it never fires — there is nothing to react to. It cannot do calendar maintenance, because health is not a date. The timer is calendar-driven: 00:00 arrives, the restart runs, healthy or not. Keep both if you need both — Restart=always for crashes, the timer for the nightly reset. They watch different clocks and never fight.

How do I check it actually fired?

systemctl list-timers app-restart.timer --no-pager
journalctl -u app-restart.service --since today --no-pager

verify: after midnight, NEXT is repopulated for tomorrow and LAST sits inside your delay window. The journal line that matters reads Starting scheduled restart of app... — followed by the app's own stop and start:

the app's own journal around a bench fire, unedited
Nov 03 05:13:48.990989 bench-arch systemd[1]: Stopping the app that gets restarted nightly...
Nov 03 05:13:48.993916 bench-arch systemd[1]: Started the app that gets restarted nightly.

A nightly restart is a confession: something leaks, and nobody has found the hole yet.

Flip your service tonight with the checklist and let the timer carry the confession until someone fixes the leak for real — then delete both units and let the box sleep through midnight. That is the exit condition, and the stamp file will not miss you.

The bench method, so you can read the raw receipts

Throwaway Arch VM (quickemu cloud image, no host changes): one app.service running sleep infinity, the timer recipe scaled to OnCalendar=*-*-* *:*:30 with RandomizedDelaySec=20 so a fire lands inside a minute of waiting, then 95 seconds of journal watching. Three fires, the stamp file, the list-timers columns and the systemd version are all in one receipt log, printed unedited above. The third bench fire landed on a clock the VM was still settling at boot; its arithmetic is the one line this piece does not defend, and the log keeps it.