Vor zwei Jahren ist mir ein Backup-Skript zum Verhängnis geworden, das per cron um 2 Uhr nachts lief. Der Kunde hatte einen Laptop als „Server" im Einsatz — ja, ich weiß —, und der Laptop wurde jeden Abend zugeklappt. Der Cronjob startete brav, aber der Rechner schlief. Punkt. Um 2 Uhr passierte nichts, weil der Rechner im Suspend hing. Und weil cron keine Erinnerung hat, blieb es dabei: Das Backup lief wochenlang nicht, und niemand merkte es, weil die Logs in einer MAILTO-Datei verschwanden, die keiner las. Seitdem nutze ich für zeitgesteuerte Jobs systemd-Timer — und dieser Artikel erklärt, warum.
systemd-Timer gibt es seit 2014 (systemd 213), und sie können fast alles, was cron kann — nur mit den Vorteilen, die das systemd-Ökosystem mitbringt: sauberes Logging über journald, echte Abhängigkeiten, Umgebungsdateien und vor allem Persistent=true, das verpasste Jobs nachholt. Dieser Artikel zeigt die Umstellung anhand meiner eigenen Timer-Sammlung: Backups, Updates, Log-Rotation und Passwort-Rotation.
Das Grundprinzip: Timer und Service sind zwei Units
Der wichtigste Unterschied zu cron ist architektonisch: Ein cron-Job ist eine Zeile in einer Konfigurationsdatei. Ein systemd-Timer ist ein Paar aus zwei Units — eine .timer-Datei, die den Zeitplan definiert, und eine .service-Datei, die den Job beschreibt. Der Timer startet den Service; der Service macht die Arbeit.
Ein minimales Beispiel, ein tägliches Backup um 2 Uhr:
# /etc/systemd/system/backup.timer
[Unit]
Description=Tägliches Backup
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomDelaySec=15m
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Führt das Backup aus
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
EnvironmentFile=/etc/backup.env
Aktiviert wird das Ganze mit:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
Warum diese Trennung gut ist, merkt man beim Debuggen: systemctl status backup.service zeigt dir den letzten Lauf, journalctl -u backup.service -e die kompletten Logs — ohne dass du irgendwo MAILTO konfigurieren musst. Der Timer ist nur noch der Wecker, der Service das, was geweckt wird.
Persistent=true: Die Laptop-Falle ist Geschichte
Das Feature, das mich damals vor dem Backup-Desaster bewahrt hätte, heißt Persistent=true. Es bedeutet: Wenn der Rechner zum geplanten Zeitpunkt aus war (oder im Suspend hing), wird der Job nach dem nächsten Start nachgeholt. Der Timer merkt sich sozusagen den verpassten Termin und holt ihn nach, sobald der Rechner wieder läuft.
Die Alternative in der cron-Welt wäre anacron gewesen — ein Werkzeug, das genau dafür gedacht ist, aber eine zweite Software, eine zweite Konfiguration und eine zweite Fehlerquelle bedeutet. Bei systemd bekommst du das Verhalten mit einer Zeile in der Timer-Datei.
Seit der Umstellung läuft das Backup meines „Laptop-Servers" zuverlässig: Der Rechner wird abends zugeklappt, am nächsten Morgen hochgefahren, und innerhalb von Minuten nach dem Boot startet das versäumte Backup. Der User merkt kurz die Festplatte, dann ist Ruhe. Für Desktops und Laptops mit unregelmäßigen Laufzeiten ist Persistent=true der Unterschied zwischen „Backup läuft" und „Backup soll laufen".
RandomDelaySec: Gegen die 3-Uhr-Lastspitze
Wer 50 oder 100 Server betreibt und auf allen um 3 Uhr nachts Updates fahren lässt, kennt das Phänomen: Die Mirror-Server stehen, die Leitung ist voll, und alle Maschinen konkurrieren gleichzeitig. Der Klassiker 0 3 * * * bei cron ist eine Einladung zur synchronen Lastspitze.
systemd löst das mit RandomDelaySec=15m (oder RandomizedDelaySec= in neueren Versionen). Der Timer wartet nach dem geplanten Zeitpunkt eine zufällige Zeit zwischen 0 und 15 Minuten, bevor er den Service startet. Bei 100 Servern verteilt das die Last über ein Viertelstunden-Fenster — die Mirror-Server atmen auf. Bei cron müsstest du dafür 100 verschiedene Cron-Zeilen mit manuell gestreuten Zeiten pflegen. Ja, ich habe das früher wirklich gemacht, mit einem Script, das pro Host eine zufällige Minute generierte. Die systemd-Lösung ist eine Zeile.
OnCalendar: Zeitangaben, die man lesen kann
Die cron-Syntax 0 3 * * 1 bedeutet „Montag 3 Uhr" — wenn man es weiß. Die OnCalendar-Syntax von systemd liest sich wie ein Satz: Mon..Fri 03:00:00, *-*-* 02:00:00, Sun 05:00. Es gibt auch relative Zeiträume für Dinge, die cron nie konnte:
- OnUnitActiveSec=1d — ein Tag nach dem letzten Lauf, nicht nach Kalenderzeit. Perfekt für Wartungsjobs, die im Abstand laufen sollen, egal wann der letzte Lauf war.
- OnBootSec=10min — zehn Minuten nach dem Boot. Für Jobs, die nach jedem Start laufen sollen.
- OnCalendar=*-*-* 02:00 — täglich um 2 Uhr. Die Wildcards erlauben „jeden Tag", „jeden Monat", „jeden Wochentag" in lesbarer Form.
Und wenn du dir unsicher bist, ob deine Zeitangabe stimmt, hilft die eingebaute Vorschau:
systemd-analyze calendar "Mon..Fri 03:00:00"
# → Normalized form: Mon..Fri 03:00:00
# → Next elapse: Mo 2026-10-05 03:00:00 CEST
# → (in UTC) Mon 2026-10-05 01:00:00 UTC
Das ist der Moment, in dem man merkt, wie viel besser die Welt sein könnte: Bei cron bekommst du keine Vorschau — du schreibst 0 3 * * 1 und hoffst.
Logging: journald statt MAILTO-Ruinen
Der zweite große Gewinn ist das Logging. Cron schickt die Ausgabe eines Jobs wahlweise ins Nirwana oder an eine MAILTO-Adresse — und die Mails von vor vier Jahren liest niemand mehr. systemd fängt die Standard-Ausgabe und -Fehler jedes Service in journald auf:
# Letzten Lauf ansehen
journalctl -u backup.service -e
# Alle Fehler der letzten Woche
journalctl -u backup.service --since "1 week ago" -p err
# Exit-Codes im Blick
systemctl status backup.service
Für mich ist das der entscheidende Unterschied im Alltag: Ich muss nichts konfigurieren, um zu sehen, ob ein Job gelaufen ist und was er gemacht hat. systemctl status backup.service sagt mir in einer Sekunde: aktiv (exited), vor 22 Stunden gelaufen, Exit-Code 0. Und wenn etwas schiefgeht, steht die Fehlermeldung im Journal — nicht in einer Mail, die im Spam gelandet ist.
Abhängigkeiten und Umgebung: Jobs, die aufeinander warten
In der cron-Welt laufen Jobs isoliert und parallel. Wenn dein Backup-Export erst nach dem Datenbank-Dump laufen soll, musst du im Skript warten oder auf Zeit-Poker setzen. systemd kennt echte Abhängigkeiten:
# backup.service
[Unit]
Description=Backup nach dem DB-Dump
After=mysqldump.service
Requires=mysqldump.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Damit ist die Reihenfolge garantiert: mysqldump.service läuft zuerst, backup.service danach — und wenn der Dump fehlschlägt, startet das Backup gar nicht erst. In cron müsstest du das in einem einzigen Monsterskript mit &&-Verkettung oder Exit-Code-Prüfung nachbauen.
Dazu kommt EnvironmentFile=: Die Variablen für den Job liegen sauber in einer eigenen Datei mit chmod 600 — keine Passwörter mehr in der Cron-Zeile, die jeder mit ps lesen kann. Ein wöchentlicher Timer, der Datenbank-Passwörter rotiert, zieht sich die neuen Werte aus der Environment-Datei und generiert die nächste Generation mit dem bitcalc Passwort-Generator — 32 Zeichen, alle Klassen, direkt in die Datei geschrieben.
Der Vergleich: cron vs. systemd-Timer in einer Tabelle
Die Tabelle zeigt die sieben Dimensionen, in denen systemd-Timer klar gewinnen — und die ehrliche Antwort auf die Frage, wo cron noch lebt: auf Systemen ohne systemd (BSD, BusyBox-Container, Alpine), in Bestands-Umgebungen, die niemand anfassen will, und bei den wenigen, die mit einer einzelnen Cron-Zeile schneller fertig sind als mit zwei Unit-Dateien. Für alles, was mehr als „ein Befehl einmal am Tag" ist, sind Timer die bessere Wahl.
Die Off-Peak-Timeline: mein nächtlicher Ablauf
Damit du siehst, wie das Ganze in der Praxis aussieht, hier mein nächtlicher Timer-Plan auf dem Homeserver:
Der Ablauf folgt einer Logik: Erst die Daten sichern, dann das System aktualisieren, dann aufräumen. Das Backup-Fenster ist dabei der kritische Pfad — ich habe ausgerechnet, dass der Transfer von 4 TB über meine Leitung mit realistischen Durchsatz-Verlusten rund 25 Minuten dauert. Dafür gibt es den bitcalc Downloadzeit-Rechner: Dateigröße, Leitung und Overhead eintragen, und du weißt, ob das nächtliche Fenster reicht — oder ob du das Backup früher starten musst. Bei mir passt es mit Puffer, aber ohne die Rechnerei hätte ich das nur geraten.
Die Timer dazu in Kurzform:
# backup.timer
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomDelaySec=15m
# update.timer
OnCalendar=Mon..Fri 03:00:00
# logrotate.timer (aktiviert den systemd-eigenen)
OnCalendar=*-*-* 04:00:00
Übrigens: Wenn du einen Job nur einmal ausführen willst, ohne eine Unit anzulegen, gibt es systemd-run — der temporäre Timer für die Kommandozeile:
# In 10 Minuten einmalig ausführen
systemd-run --on-calendar "*-*-* *:00:10" /usr/local/bin/cleanup.sh
# Nach dem Boot einmalig
systemd-run --on-boot=5min /usr/local/bin/cleanup.sh
Das ersetzt das alte at-Kommando und ist trotzdem journald-protokolliert.
Wann cron doch besser ist — die ehrliche Antwort
Ich will nicht so tun, als wäre cron Müll. Drei Situationen, in denen ich cron weiterhin verwende:
- Minimal-Systeme ohne systemd: BusyBox-Container, Alpine im Docker, BSD-Jails. Da gibt es systemd schlicht nicht, und cron (oder BusyBox crond) ist die pragmatische Wahl.
- Einzeiler-Wartung: Ein Job, der aus genau einem Befehl besteht und nie wächst? Eine Cron-Zeile in
/etc/cron.dist schneller geschrieben als zwei Unit-Dateien. Solange er nicht wächst. - Bestehende Umgebungen: Wenn ein Team seit zehn Jahren mit cron lebt und es funktioniert, ist die Migration nicht der richtige erste Schritt. Timer einführen beim nächsten Neubau, nicht im laufenden Betrieb umgraben.
In meinem eigenen Bestand gilt seit zwei Jahren: Jeder neue zeitgesteuerte Job ist ein Timer. Die Migration der Altlasten passiert nach und nach, wenn ich die Datei eh anfasse.
Fazit
systemd-Timer sind die Antwort auf die Fragen, die cron seit Jahrzehnten offen lässt: Was passiert, wenn der Rechner aus war? Wie verteile ich Last? Wo sind die Logs? OnCalendar liest sich wie Deutsch, Persistent=true holt verpasste Jobs nach, RandomDelaySec entzerrt Lastspitzen, und journald protokolliert alles mit. Die Trennung von Timer und Service ist am Anfang etwas gewöhnungsbedürftig, zahlt sich aber beim Debuggen sofort aus.
Mein Backup-Desaster von damals hätte eine einzige Zeile verhindert. Seit die Timer laufen, schaue ich morgens kurz auf systemctl list-timers — und weiß, dass die Nacht ihre Arbeit getan hat, auch wenn der Laptop zugemacht war.