Cron-Jobs überwachen: Fehlgeschlagene Jobs zuverlässig erkennen
AI generated
$
/etc
Linux · Cron · Monitoring · Systemadministration
Cron-Jobs überwachen
wenn Fehlschläge nicht einfach verschwinden dürfen

Ein Cron-Job, der lautlos scheitert, hinterlässt keine Fehlermeldung auf einem Bildschirm, den jemand liest. Dieser Leitfaden zeigt, wie MAILTO tatsächlich funktioniert, wie Exit-Codes in Cron-Skripten ausgewertet werden, wie Healthcheck-Pings als Dead Man Switch fehlende Ausführungen erkennen und wie all das zu einer verlässlichen Alerting-Kette für Cron-Jobs zusammenwächst.

14 Min. Lesezeit MAILTO · Exit-Codes · Healthchecks · Alerting Linux · Debian/Ubuntu · Produktivbetrieb

1. Warum Cron-Jobs unbemerkt scheitern

Ein Cron-Job läuft ohne Terminal, ohne sichtbare Konsole und meistens ohne jemanden, der zufällig gerade zusieht. Genau das macht Cron-Job-Monitoring zu einer eigenen Disziplin: Ein Skript, das im interaktiven Betrieb sofort auffallen würde, weil eine Fehlermeldung im Terminal erscheint, kann als Cron-Job wochenlang scheitern, ohne dass irgendjemand etwas bemerkt. Die Standardausgabe eines Cron-Jobs verschwindet standardmäßig, wenn kein MAILTO gesetzt ist oder wenn die lokale Mail-Zustellung nie funktioniert hat, was in frisch aufgesetzten Containern und minimalen Serverinstallationen der Normalfall ist.

Das Ergebnis sieht man oft erst, wenn der Schaden längst eingetreten ist: ein Backup, das seit drei Wochen keine neuen Dateien mehr erzeugt, ein Datenbank-Export, der wegen eines vollen Datenträgers fehlschlägt, oder ein Zertifikat, das nicht erneuert wurde, weil der Renewal-Cronjob still an einem geänderten Pfad gescheitert ist. Kein Betriebssystem meldet solche Fehler von sich aus, denn aus Sicht von Cron selbst ist ein fehlgeschlagener Job kein besonderes Ereignis, sondern lediglich ein Exit-Code ungleich null, den standardmäßig niemand auswertet.

Ein wirksames Cron-Job-Monitoring braucht deshalb mehrere zusammenwirkende Bausteine: eine funktionierende Zustellung von Fehlermeldungen, eine bewusste Auswertung von Exit-Codes, ein Mechanismus, der auch das komplette Ausbleiben einer Ausführung erkennt, sowie eine Alerting-Kette, die Menschen tatsächlich erreicht, statt in einem ungelesenen Postfach zu verschwinden. Die folgenden Abschnitte bauen diese Bausteine Schritt für Schritt auf.

2. MAILTO und lokale Mail-Zustellung richtig konfigurieren

Die klassische Cron-Variable MAILTO steuert, an welche Adresse Cron die Standardausgabe und die Standardfehlerausgabe eines Jobs schickt, sofern der Job überhaupt Ausgabe produziert. Gesetzt wird sie als eigene Zeile am Anfang einer Crontab: MAILTO=admin@example.com. Wichtig zu verstehen: Cron verschickt nur dann eine Mail, wenn der Job tatsächlich etwas auf stdout oder stderr ausgibt. Ein Skript, das bei einem Fehler lautlos mit einem Nicht-Null-Exit-Code endet, ohne eine einzige Zeile Text zu schreiben, erzeugt trotz Fehler keine Mail, denn für Cron-Job-Monitoring per MAILTO zählt allein die Textausgabe, nicht der Exit-Code selbst.

Der zweite, in der Praxis häufigere Stolperstein: MAILTO ist korrekt gesetzt, aber der lokale Mail-Transport-Agent fehlt komplett. Minimal installierte Server und die meisten Docker-Images enthalten keinen sendmail-kompatiblen MTA, wodurch Cron beim Versand scheitert, ohne dass dies irgendwo sichtbar protokolliert wird. Für produktive Systeme lohnt sich daher entweder die Installation eines schlanken Weiterleiters wie msmtp, der lokale Mail direkt an einen externen SMTP-Server durchreicht, oder der komplette Verzicht auf MAILTO zugunsten von Healthcheck-Pings und externem Alerting, wie in den folgenden Abschnitten beschrieben.


# /etc/msmtprc: minimal relay so cron mail actually leaves the host
# Cron itself just calls "sendmail" internally, msmtp intercepts that call

defaults
auth           on
tls            on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile        /var/log/msmtp.log

account        default
host           smtp.example.com
port           587
from           cron-alerts@mironsoft.de
user           cron-alerts@mironsoft.de
password       CHANGE_ME_use_a_secrets_manager

# Make msmtp the system-wide sendmail replacement
# ln -sf /usr/bin/msmtp /usr/sbin/sendmail

# In the crontab, set MAILTO explicitly and test with:
# echo "test body" | mail -s "cron mail test" admin@example.com

Wer bewusst keine Mail-Zustellung einrichten will, sollte MAILTO explizit auf einen leeren String setzen: MAILTO="". Das unterdrückt Zustellversuche vollständig und vermeidet kryptische Fehlermeldungen im Systemlog, die sonst bei jedem gescheiterten Sendversuch entstehen. Diese explizite Entscheidung ist Teil eines bewussten Cron-Job-Monitoring-Konzepts, bei dem die Alerting-Verantwortung komplett an Healthchecks und strukturiertes Logging übergeben wird.

3. Exit-Codes in Cron-Skripten korrekt auswerten

Jeder Cron-Job endet mit einem Exit-Code zwischen null und 255. Null bedeutet Erfolg, jeder andere Wert signalisiert einen Fehlerzustand, den das aufrufende Skript selbst festlegt. Das Problem in der Praxis: Viele Cron-Skripte bestehen aus mehreren aufeinanderfolgenden Befehlen, und nur der Exit-Code des letzten Befehls bestimmt, was Cron insgesamt sieht. Ein Backup-Skript, das einen Datenbank-Dump erzeugt und anschließend erfolgreich komprimiert, meldet vollen Erfolg, selbst wenn der Dump-Befehl selbst mit einem Fehler abgebrochen ist, solange die anschließende Komprimierung des leeren oder unvollständigen Ergebnisses fehlerfrei durchläuft.

Die zuverlässige Lösung für dieses Cron-Job-Monitoring-Problem ist set -euo pipefail am Anfang jedes Skripts, kombiniert mit einer expliziten Prüfung des Exit-Codes nach jedem kritischen Schritt. Zusätzlich lohnt sich ein einheitliches Wrapper-Muster: Jeder Cron-Job ruft nicht das eigentliche Skript direkt auf, sondern eine kleine Wrapper-Funktion, die den finalen Exit-Code auswertet, eine strukturierte Logzeile schreibt und bei Bedarf einen Healthcheck-Ping auslöst. So wird die Auswertung des Exit-Codes zu einer wiederverwendbaren Infrastrukturkomponente statt zu einer Ad-hoc-Lösung in jedem einzelnen Skript.


#!/usr/bin/env bash
# cron-wrapper.sh — wraps any cron job, evaluates the real exit code
set -euo pipefail

readonly JOB_NAME="${1:?Usage: cron-wrapper.sh <job-name> <command...>}"
shift
readonly LOG_FILE="/var/log/cron-jobs/${JOB_NAME}.log"
readonly START_TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

mkdir -p "$(dirname "$LOG_FILE")"

# Run the actual command, capture combined output and exit code
set +e
output="$("$@" 2>&1)"
exit_code=$?
set -e

end_ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

{
  echo "[${START_TS}] job=${JOB_NAME} start"
  echo "$output"
  echo "[${end_ts}] job=${JOB_NAME} exit_code=${exit_code}"
} >> "$LOG_FILE"

if [[ $exit_code -ne 0 ]]; then
  logger -t "cron-${JOB_NAME}" "FAILED with exit code ${exit_code}"
  exit "$exit_code"
fi

logger -t "cron-${JOB_NAME}" "OK"

4. Healthcheck-Pings als Dead Man Switch

Exit-Codes und MAILTO lösen nur eine Hälfte des Problems: Sie erkennen, wenn ein Cron-Job läuft und scheitert. Sie erkennen aber nicht, wenn ein Cron-Job überhaupt nicht mehr läuft, weil die Crontab versehentlich gelöscht wurde, weil der Cron-Daemon nach einem Neustart nicht wieder gestartet ist, oder weil ein Deployment die Crontab-Zeile durch einen Tippfehler unwirksam gemacht hat. Genau für dieses Szenario existiert das Konzept des Dead Man Switch: Ein externer Dienst erwartet in regelmäßigen Abständen ein Signal vom Job und schlägt Alarm, sobald dieses Signal ausbleibt, statt nur auf explizite Fehlermeldungen zu reagieren.

Dienste wie Healthchecks.io oder eine selbst gehostete Alternative funktionieren nach demselben Prinzip: Der Cron-Job ruft am erfolgreichen Ende eine eindeutige URL per curl auf, der Dienst merkt sich den Zeitpunkt und vergleicht ihn mit einem erwarteten Intervall. Bleibt der Ping länger aus als erwartet, etwa weil der Job komplett nicht mehr ausgeführt wird, löst der Dienst eine Benachrichtigung per E-Mail, Slack oder Webhook aus. Dieses Muster ergänzt Cron-Job-Monitoring um genau die Lücke, die reine Exit-Code-Prüfung nicht abdecken kann: das vollständige Ausbleiben einer Ausführung.


#!/usr/bin/env bash
# backup-with-healthcheck.sh — ping a dead man switch on success and start
set -euo pipefail

readonly HEALTHCHECK_URL="https://hc-ping.com/CHANGE-ME-uuid"
readonly BACKUP_DIR="/var/backups/mysql"

# Signal "job started" — lets the healthcheck service flag long-running jobs
curl -fsS -m 10 --retry 3 "${HEALTHCHECK_URL}/start" > /dev/null || true

if mysqldump --all-databases | gzip > "${BACKUP_DIR}/dump-$(date +%F).sql.gz"; then
  # Success ping — resets the "missed check-in" timer on the healthcheck side
  curl -fsS -m 10 --retry 3 "${HEALTHCHECK_URL}" > /dev/null || true
else
  # Failure ping with exit code — triggers an immediate alert
  curl -fsS -m 10 --retry 3 "${HEALTHCHECK_URL}/fail" > /dev/null || true
  exit 1
fi

5. Strukturiertes Logging für Cron-Jobs

Eine einzelne fehlende Mail oder ein einzelner ausgebliebener Healthcheck-Ping erklärt selten, warum ein Job gescheitert ist. Für eine echte Diagnose braucht Cron-Job-Monitoring nachvollziehbare Logzeilen mit Zeitstempel, Job-Name, Laufzeit und Exit-Code, konsistent über alle Cron-Jobs eines Servers hinweg. Ein einheitliches Format wie timestamp job_name exit_code duration_ms lässt sich mit einfachen Textwerkzeugen wie grep und awk auswerten, ohne dass ein separates Log-Aggregationssystem zwingend nötig ist, gerade auf kleineren Servern mit überschaubarer Job-Anzahl.

Entscheidend ist, dass jeder Cron-Job durch denselben Wrapper läuft, statt dass jedes Skript sein eigenes Logging-Format erfindet. Ein zentraler Wrapper, wie im vorherigen Abschnitt gezeigt, garantiert konsistente Logzeilen und macht es möglich, mit einem einzigen Kommando alle in der letzten Woche fehlgeschlagenen Jobs über alle Cron-Einträge hinweg zu finden, unabhängig davon, wer welches Skript ursprünglich geschrieben hat.

6. Cron-Ausgaben an das systemd-Journal anbinden

Auf systemd-basierten Distributionen lässt sich Cron-Job-Monitoring zusätzlich an das zentrale Journal anbinden, statt separate Logdateien in /var/log/cron-jobs zu pflegen. Der Befehl logger -t cron-jobname "Nachricht" schreibt eine Zeile direkt ins Journal, versehen mit einem eigenen Tag, wodurch sich einzelne Jobs gezielt mit journalctl -t cron-jobname filtern lassen. Dieser Ansatz vereint Cron-Ausgaben mit den übrigen Systemlogs an einem Ort und profitiert von der automatischen Rotation und Größenbegrenzung, die das Journal ohnehin mitbringt.

Ein praktischer Vorteil gegenüber reinen Textdateien: journalctl erlaubt Zeitraumfilter wie --since "1 hour ago" und die Kombination mit -p err, um ausschließlich Fehlermeldungen anzuzeigen, ohne selbst nach bestimmten Textmustern grep zu müssen. Für Server, die bereits zentrales Log-Shipping betreiben, etwa via systemd-journal-remote oder einen separaten Log-Collector, landen Cron-Fehler damit automatisch in derselben Pipeline wie alle anderen Systemereignisse, ohne eine gesonderte Integration für Cron zu benötigen.

7. Von der Logzeile zur Benachrichtigung

Eine Logzeile allein alarmiert niemanden, solange sie nicht aktiv ausgewertet wird. Der letzte Baustein eines vollständigen Cron-Job-Monitoring-Setups ist deshalb eine Alerting-Pipeline, die fehlgeschlagene Jobs aktiv an einen Kanal weiterleitet, den Menschen tatsächlich beachten, typischerweise ein Team-Chat wie Slack oder Mattermost statt einer E-Mail-Adresse, die in der täglichen Flut untergeht. Ein einfacher Webhook-Aufruf direkt aus dem Cron-Wrapper heraus erreicht dieses Ziel ohne zusätzliche Softwareinstallation.

Für größere Umgebungen mit vielen Servern lohnt sich stattdessen die Integration in ein zentrales Monitoring-System wie Prometheus mit dem Node Exporter Textfile Collector: Der Cron-Wrapper schreibt nach jedem Lauf eine Metrikdatei mit Zeitstempel und Exit-Code, Prometheus liest diese Datei ein, und Alertmanager löst bei ausbleibenden oder fehlgeschlagenen Jobs eine Benachrichtigung nach den bereits etablierten Eskalationsregeln des Teams aus. So fügt sich Cron-Job-Monitoring in eine bestehende Monitoring-Landschaft ein, statt einen isolierten Sonderweg zu bilden.


{
  "text": "Cron job failed: nightly-backup",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "*Cron-Job fehlgeschlagen*\nJob: `nightly-backup`\nExit-Code: `1`\nHost: `db-primary-01`\nZeitpunkt: `2026-07-31T02:15:03Z`"
      }
    }
  ]
}

8. Laufzeiten überwachen und stille Hänger erkennen

Ein Job, der weder abstürzt noch einen Fehler meldet, aber seit Stunden läuft, ohne fertig zu werden, entgeht sowohl der Exit-Code-Prüfung als auch dem klassischen Healthcheck-Ping, solange dieser erst am Ende geschickt wird. Der Befehl timeout erzwingt eine harte Obergrenze für die Laufzeit eines Jobs und beendet ihn mit einem definierten Exit-Code, sobald diese Grenze überschritten wird, wodurch ein hängender Prozess nicht unbegrenzt Ressourcen bindet und stattdessen als regulärer Fehlschlag im Cron-Job-Monitoring erscheint.

Zusätzlich lohnt sich der Vergleich der tatsächlichen Laufzeit mit einem erwarteten Wert. Ein nächtlicher Datenbank-Export, der normalerweise fünf Minuten dauert und plötzlich vierzig Minuten braucht, deutet meist auf ein wachsendes Datenvolumen, einen fehlenden Index oder ein anderes schleichendes Problem hin, lange bevor der Job tatsächlich fehlschlägt. Der Start-Ping aus Abschnitt vier, kombiniert mit einem konfigurierten Zeitfenster im Healthcheck-Dienst, deckt genau dieses Szenario ab, ohne dass eine eigene Zeitmessung im Skript nötig wäre.

9. Monitoring-Ansätze im Vergleich

Kein einzelner Ansatz deckt alle Ausfallszenarien eines Cron-Jobs ab. Die folgende Tabelle stellt die Bausteine gegenüber und zeigt, welche Fehlerklasse jeweils erkannt wird.

Ansatz Erkennt Erkennt NICHT Aufwand
MAILTO Ausgabe bei Fehlern, sofern MTA funktioniert Ausbleibende Ausführung, lautlose Fehler Gering, aber MTA-Setup nötig
Exit-Code-Wrapper Echte Fehlerzustände einzelner Schritte Ausbleibende Ausführung des Jobs selbst Mittel, ein Wrapper-Skript
Healthcheck-Ping Ausbleibende und fehlgeschlagene Ausführung Interne Logik-Fehler ohne Exit-Code-Weitergabe Gering, ein curl-Aufruf
timeout-Guard Hängende, nie beendende Jobs Logisch falsche, aber schnell beendete Läufe Sehr gering, ein Flag
Prometheus/Alertmanager Alle obigen Fälle zentral pro Server Nichts, aber höherer Infrastrukturaufwand Hoch, eigene Monitoring-Stack-Pflege

In der Praxis kombiniert man meist mehrere Ansätze: einen Wrapper für konsistentes Logging und Exit-Code-Auswertung, einen Healthcheck-Ping für das Erkennen ausbleibender Ausführungen und bei wachsender Serverzahl zusätzlich eine zentrale Monitoring-Lösung. Die einzelnen Bausteine schließen sich nicht gegenseitig aus, sondern ergänzen sich zu einem Cron-Job-Monitoring, das sowohl stille als auch laute Fehlschläge zuverlässig meldet.

Mironsoft

Server-Automatisierung, Monitoring und Alerting für Linux-Infrastruktur

Cron-Jobs, die endlich zuverlässig überwacht werden?

Wir richten Healthcheck-Pings, strukturiertes Logging und Alerting-Ketten für eure Cron-Jobs und systemd-Timer ein, sodass Fehlschläge und ausbleibende Ausführungen zuverlässig gemeldet werden, statt in Postfächern zu verschwinden.

Monitoring-Audit

Bestandsaufnahme aller Cron-Jobs und deren aktueller Fehlerbehandlung

Wrapper & Healthchecks

Einheitliche Logging-Wrapper und Dead Man Switch Integration einrichten

Alerting-Integration

Anbindung an Slack, Prometheus Alertmanager oder bestehende Monitoring-Tools

10. Zusammenfassung

Cron-Job-Monitoring braucht mehr als eine gesetzte MAILTO-Variable, denn diese erkennt weder ausbleibende Ausführungen noch lautlose Fehler ohne Textausgabe. Ein Exit-Code-Wrapper mit strukturiertem Logging macht Fehlerzustände nachvollziehbar und konsistent über alle Jobs eines Servers hinweg. Healthcheck-Pings schließen die entscheidende Lücke des Dead Man Switch, indem sie auch das vollständige Ausbleiben eines Jobs erkennen, nicht nur dessen Fehlschlag.

Der timeout-Befehl verhindert, dass hängende Jobs unbegrenzt Ressourcen binden, während die Anbindung an das systemd-Journal oder ein zentrales Monitoring-System wie Prometheus die Auswertung über viele Server hinweg vereinheitlicht. Wer diese Bausteine kombiniert, erreicht ein Cron-Job-Monitoring, das sowohl laute Fehler als auch die deutlich gefährlicheren stillen Fehlschläge zuverlässig an die richtigen Menschen meldet.

Cron-Job-Monitoring, das Wichtigste auf einen Blick

MAILTO reicht nicht

MAILTO meldet nur Jobs mit Textausgabe und funktionierendem MTA, nicht ausbleibende Ausführungen.

Wrapper-Pattern

Ein einheitlicher Wrapper wertet Exit-Codes aus und schreibt konsistente, strukturierte Logzeilen.

Dead Man Switch

Healthcheck-Pings erkennen als einzige Methode auch das komplette Ausbleiben eines Jobs.

timeout & Alerting

timeout begrenzt Laufzeiten, Slack- oder Prometheus-Integration bringt Fehler tatsächlich vor Menschen.

11. FAQ: Cron-Job-Monitoring

1Warum bekomme ich keine Mail bei einem Fehler?
MAILTO verschickt nur bei tatsächlicher Textausgabe und funktionierendem Mail-Transport-Agent. Lautlose Fehler ohne Ausgabe erzeugen keine Mail.
2Wie erkenne ich einen komplett ausbleibenden Job?
Nur ein externer Healthcheck-Ping erkennt ausbleibende Ausführungen zuverlässig.
3Was ist ein Dead Man Switch?
Ein Mechanismus, der Alarm schlägt, sobald ein erwartetes Signal ausbleibt, statt nur auf Fehler zu reagieren.
4Warum reicht der letzte Exit-Code in einer Kette nicht?
Ohne pipefail zählt nur der letzte Befehl. set -euo pipefail behebt dieses Verhalten.
5Wie verhindere ich unbegrenzt hängende Jobs?
Mit dem Befehl timeout eine harte Laufzeitgrenze setzen.
6Dateien oder Journal für Cron-Logs?
Beides funktioniert, wichtig ist Konsistenz über alle Jobs eines Servers hinweg.
7Wie leite ich Fehler an Slack weiter?
Ein curl-Aufruf gegen die Incoming-Webhook-URL im Wrapper genügt.
8Lohnt sich Prometheus auf einem einzelnen Server?
Meist nicht, der Aufwand lohnt sich erst ab mehreren Servern mit vielen Jobs.
9Braucht jeder Job eine eigene Ping-URL?
Ja, damit individuelle Zeitfenster und Alarme pro Job geführt werden können.
10Wie deaktiviere ich MAILTO sauber?
MAILTO="" explizit setzen unterdrückt Zustellversuche vollständig.