Serverless-Cron-Jobs: Überlappungen verhindern und stille Fehler erkennen
Serverlose Cron-Jobs zuverlässig machen: Scheduler wählen, parallele Läufe mit Locks verhindern und einen „last ran“-Heartbeat mit Alerts hinzufügen.

Das Problem: Überlappungen und stille Fehler
Die meisten serverlosen Cron-Jobs scheitern auf zwei vorhersehbare Arten: Sie laufen gleichzeitig zweimal oder sie hören auf zu laufen und niemand merkt es.
Eine Überlappung entsteht, wenn der nächste geplante Lauf startet, bevor der vorherige fertig ist. In echten Systemen führt das zu doppelten Rechnungen, wiederholten E-Mails, doppelten Auszahlungen oder Importen, die dieselben Zeilen zweimal schreiben. Selbst wenn Ihr Code größtenteils idempotent ist, schaden Überlappungen trotzdem: Sie verbrauchen Rate-Limits, belasten Karten doppelt und halten Sperren länger als erwartet.
Stille Fehler sind schlimmer, weil es so aussieht, als wäre nichts passiert. Ein Job kann stoppen, weil ein Deployment den Zeitplan entfernt hat, eine Berechtigungsänderung den Datenbankzugriff blockiert, ein Secret abgelaufen ist, Kontingente erreicht wurden oder ein Plattform-Update einen Trigger deaktiviert hat. Alte Logs sind vielleicht noch da, also scheint alles in Ordnung — bis ein Kunde fehlende Daten meldet.
"Es hat einmal funktioniert" ist kein Zuverlässigkeitsplan. Ein Job, der gestern lief, ist kein Beweis dafür, dass er morgen läuft, besonders wenn Konfiguration, Berechtigungen, Secrets und Laufzeitumgebungen sich ändern, ohne dass am Code etwas verändert wurde.
Was Sie wollen, ist einfach und messbar:
- Keine gleichzeitigen Läufe (ein Lauf übernimmt die Arbeit, alle anderen treten zurück)
- Schnelle Erkennung, wenn Läufe aufhören (ein klares "zuletzt ausgeführt"-Signal plus ein Alert, wenn es veraltet ist)
Behandeln Sie Scheduling wie Produktionsinfrastruktur, dann hören Sie später auf, seltsamen, intermittierenden Bugs hinterherzurennen.
Wählen Sie einen Scheduler, der zum Job passt
Nicht alle Scheduler verhalten sich gleich — und das wird wichtig, sobald Leute von Ihren Jobs abhängig sind.
Ereignisbasierte Scheduler feuern einen Trigger zu (etwa) einem bestimmten Zeitpunkt und übergeben die Arbeit an eine Funktion oder ein Endpoint. Sie sind einfach und günstig, aber die Zustellung ist oft "best effort", es sei denn, Sie fügen Retries, Dead-Letter-Handhabung und Monitoring hinzu.
Warteschlangenbasierte Scheduler stellen eine Nachricht "diesen Job ausführen" in eine Queue. Dieser zusätzliche Schritt ist nützlich, weil Queues normalerweise bessere Kontrolle über Retries, Backpressure und Sichtbarkeit bieten. Wenn Ihr Job schwer, langsam oder spiky ist, machen Queues Fehler sichtbarer und leichter wiederherstellbar.
Gängige Optionen sind unter anderem AWS EventBridge Scheduler (oder CloudWatch-Schedules), GCP Cloud Scheduler (oft kombiniert mit Pub/Sub oder Cloud Tasks), Azure Functions Timer Trigger (oder Logic Apps) und CI-Scheduler wie GitHub Actions für leichte Wartungsaufgaben.
Eine praktische Auswahlhilfe sind ein paar Fragen:
- Wie oft läuft er, und ist die exakte Minute wichtig?
- Wie lange kann ein Lauf im Peak dauern (Sekunden vs Stunden)?
- Was soll bei einem Fehler passieren: retry, alert oder beides?
- Welche Berechtigungen braucht er (Datenbank, Secrets, Drittanbieter-APIs)?
- Muss ein verpasster Lauf nachgeholt werden?
Wenn Sie starke Garantien brauchen, vermeiden Sie "fire and forget"-Setups ohne Retries, ohne Dead-Letter-Pfad und ohne Alert, wenn nichts läuft. Genau so hören Jobs leise tagelang auf.
Definieren Sie Ihr Laufmodell, bevor Sie coden
Viele Zuverlässigkeitsprobleme beginnen bevor der Scheduler ins Spiel kommt. Sie beginnen mit einer unscharfen Definition dessen, was ein "Lauf" überhaupt ist.
Entscheiden Sie, was als ein Lauf zählt, und schreiben Sie es auf. Ist es "alles seit dem letzten Lauf", "alle Datensätze von gestern" oder "ein Batch-ID, erstellt um 02:00"? Diese eine Entscheidung beeinflusst, wie Sie sperren, retryen und wiederherstellen.
Machen Sie "zweimal laufen" dort sicher, wo Sie können
Selbst mit guten Locks sollten Sie davon ausgehen, dass ein Lauf wegen Retries, Timeouts oder manuellen Replays zweimal passieren kann. Streben Sie idempotente Arbeit an: dieselbe Eingabe sollte zum selben Endergebnis führen.
Ein einfaches Muster ist, einen Lauf-Key zu speichern (z. B. 2026-01-20) und zu protokollieren, welche Elemente unter diesem Key verarbeitet wurden. Läuft derselbe Key erneut, überspringen Sie abgeschlossene Elemente statt Nebeneffekte zu wiederholen.
Trennen Sie Trigger und Worker
Behandeln Sie den Schedule-Trigger als dünnen Starter und legen Sie die eigentliche Arbeit in eine Worker-Funktion. Der Trigger sollte nur den Lauf-Key berechnen, versuchen, den Lauf zu beanspruchen, und dann übergeben.
Das trennt Business-Logik von Zuverlässigkeits-Guardrails und macht es einfacher, später den Scheduler zu wechseln.
Definieren Sie vor dem Coden die erwarteten Outcomes:
- Erfolg: Welche Daten sind definitiv korrekt und wo sind sie abgelegt?
- Fehler: Was muss zurückgerollt werden und was kann erneut versucht werden?
- Teilweiser Erfolg: Was ist sicher zu behalten und wie setzen Sie fort?
- Timeout: Welcher Zustand kann zurückbleiben?
Planen Sie Ihre Concurrency-Guard (Lock-Strategie)
Wenn Sie serverlose Cron-Jobs betreiben, gehen Sie davon aus, dass zwei schlimme Tage passieren: ein Job läuft lange und der nächste Zeitplan feuert trotzdem, oder eine Funktion retryt nach einem Timeout und Sie bekommen zwei Kopien. Eine Concurrency-Guard ist das kleine Stück, das diese Tage langweilig macht.
Starten Sie damit, den Ort des Locks zu wählen. Wählen Sie etwas, das Ihr Job schnell lesen und schreiben kann, mit starkem "nur einer gewinnt"-Verhalten.
Wählen Sie Ihr Lock-Store
Gängige Optionen:
- Eine einzelne Datenbankzeile (toll, wenn Sie bereits Postgres/MySQL nutzen und atomare Updates durchführen können)
- Redis (schnell und praktisch für kurze Locks, aber stellen Sie hohe Verfügbarkeit sicher)
- Objekt-Storage-Lease (ein Blob/Datei erstellt mit "if not exists"; einfach, kann aber langsamer sein)
Als Nächstes entscheiden Sie, wofür der Lock-Key steht. Ein praktischer Key enthält oft Job-Name und das geplante Zeitfenster, z. B. billing-sync:2026-01-20T02:00Z. Das blockiert Duplikate für denselben Slot, ohne den nächsten Tag zu verhindern.
Setzen Sie immer eine TTL (Ablaufzeit). TTL schützt Sie, wenn ein Lauf mitten im Prozess crasht oder die Plattform den Prozess tötet. Legen Sie sie etwas länger als Ihre Worst-Case-Laufzeit fest, nicht länger als den Durchschnitt.
Entscheiden Sie schließlich, was bei einem Konflikt passiert:
- Überspringen (sicher bei idempotenter Arbeit, aber Sie könnten einen Lauf verpassen)
- Neuer Zeitplan (bessere Abdeckung, kann aber Traffic-Spitzen erzeugen)
- Laut scheitern (am besten für kritische Jobs, bei denen ein verpasster Lauf schlimmer ist als Rauschen)
Schritt-für-Schritt: Gleichzeitige Läufe mit einem Lock verhindern
Überlappungen passieren, wenn Ihr Scheduler zweimal feuert oder ein Lauf länger als erwartet dauert. In Serverless ist die einfachste Lösung ein geteilter Lock außerhalb der Funktion (eine Datenbankzeile, ein Redis-Key oder ein Cloud-KV). Ein Lauf gewinnt den Lock; die anderen beenden sich.
1) Benutzen Sie einen klaren Lock-Flow
Halten Sie den Ablauf vorhersehbar:
- Lock erwerben (atomare Erstellung oder bedingtes Update)
- Wenn Lock belegt ist, sofort beenden
- Den Job ausführen
- Lock freigeben, aber nur wenn Sie ihn noch besitzen
2) Fügen Sie ein Owner-Token hinzu und geben Sie immer frei
Ein Owner-Token verhindert, dass Lauf B den Lock von Lauf A freigibt. Geben Sie immer im finally-Block frei, damit Fehler keinen permanenten Lock hinterlassen.
import crypto from "crypto";
export async function handler() {
const lockKey = "nightly-report";
const owner = crypto.randomUUID();
const ttlSeconds = 15 * 60; // lock safety window
const acquired = await acquireLock({ lockKey, owner, ttlSeconds });
if (!acquired) return { status: "skipped", reason: "lock_taken" };
try {
await doWork();
return { status: "ok" };
} finally {
await releaseLock({ lockKey, owner }); // only release if owner matches
}
}
Ein gutes acquireLock ist atomar und setzt ein Ablaufdatum (TTL), sodass ein abgestürzter Lauf nicht für immer blockiert.
3) Testen Sie mit erzwungener Überlappung
Starten Sie zwei Läufe gleichzeitig (manuell zweimal aufrufen oder den Zeitplan temporär verkürzen). Einer sollte laufen; der andere sollte "skipped: lock_taken" protokollieren. Wenn beide laufen, ist Ihr Lock-Write nicht wirklich atomar oder Ihre Owner-Prüfung fehlt.
Schritt-für-Schritt: Fügen Sie eine "last-ran"-Heartbeat-Prüfung hinzu
Ein Heartbeat ist ein kleines "ich bin gelaufen"-Protokoll, das Ihr Job jedes Mal schreibt, wenn er endet (Erfolg oder Fehler). Das verwandelt stille Fehler in Alerts, was in Serverless wichtig ist, da es keinen ständig laufenden Prozess gibt, der ein Stagnieren bemerkt.
1) Wählen Sie, wo Sie "last ran" speichern
Wählen Sie einen Ort, der leicht zu schreiben, schnell zu lesen und unwahrscheinlich gleichzeitig mit Ihrem Scheduler down ist:
- Eine Datenbanktabelle
- Ein Key-Value-Eintrag (einfach: "job_name -> last_run")
- Ein Metriksystem / Time-Series-Gauge
2) Protokollieren Sie die richtigen Felder
Speichern Sie nicht nur einen Zeitstempel. Schreiben Sie genug, um ohne großes Log-Digging debuggen zu können.
job_name, run_id, started_at, finished_at, status, duration_ms, error_snippet
Eine praktische Regel: schreiben Sie einmal beim Start (status=running) und aktualisieren Sie am Ende (status=success oder failed). Das lässt Sie auch "stuck running" erkennen.
3) Setzen Sie Schwellenwerte und Alert-Regeln
Setzen Sie den Threshold für "fehlenden Heartbeat" auf ungefähr das 2x Ihrer erwarteten Intervalldauer. Läuft ein Job alle 15 Minuten, alerten Sie, wenn es in 30 Minuten keinen erfolgreichen Heartbeat gab.
Unterscheiden Sie Alert-Typen:
- Fehlender Heartbeat: kein Erfolg innerhalb des Thresholds (wahrscheinlich läuft nichts)
- Wiederholte Fehler: die letzten N Läufe sind fehlgeschlagen (der Job läuft, aber die Arbeit ist kaputt)
Beispiel: Ein nächtlicher Billing-Sync sollte um 02:00 Uhr laufen und in 5 Minuten fertig sein. Alarmieren Sie, wenn bis 02:15 Uhr kein Erfolg vorliegt. Verwenden Sie einen anderen Alert, wenn er lief, aber drei Nächte hintereinander fehlschlug.
Logging und Alerts, die wirklich nützlich sind
Wenn serverlose Cron-Jobs sich falsch verhalten, ist das erste Problem meist nicht der Scheduler. Es ist, dass niemand drei Fragen schnell beantworten kann: Hat er gestartet, hat er beendet, und warum wurde er übersprungen?
Geben Sie jedem Lauf eine konsistente Run-ID (z. B. Zeitstempel + kurze zufällige Endung). Loggen Sie diese zu Beginn und nehmen Sie sie in jede Log-Zeile auf, damit Sie einen Lauf End-to-End nachverfolgen können.
Protokollieren Sie auch, wenn ein Lauf absichtlich nicht stattfindet. Ein übersprungener Lauf ist nicht dasselbe wie ein fehlgeschlagener Lauf, aber er ist trotzdem ein wichtiges Signal. Wenn der Job nicht lief, weil er den Lock nicht bekam, sagen Sie das deutlich und geben Sie den Lock-Key (und den Owner, falls vorhanden) an.
Halten Sie Logs konsistent:
- Start: run id, job name, trigger time, version/commit, wichtige Eingaben
- Skip: run id, job name, skip reason (lock conflict, scheduler disabled, feature flag off)
- Finish: run id, status (ok/failed), Dauer, Zähler (verarbeitete Elemente, Fehler)
- Failure: run id, Fehler-Typ, sicherer Kontext und was bereits getan wurde
Alerts sollten Muster überwachen, nicht nur einzelne Fehler. Ein Daueranstieg kann bedeuten, dass eine Upstream-API langsam ist. Zu viele Skips können auf einen hängenden Lock hindeuten. Keine Läufe deuten oft auf Permission-Drift im Scheduler oder ein Deployment, das den Trigger entfernte.
Machen Sie jeden Alert handlungsorientiert. Fügen Sie die letzte erfolgreiche Laufzeit, die erwartete nächste Laufzeit und die erste zu prüfende Stelle hinzu (Lock-Record, Scheduler-Status, letzter Deploy).
Retries, Timeouts und sicheres Nachholen
Retries helfen, erzeugen aber auch Überlappungen. Viele Scheduler retryen automatisch, wenn Ihre Funktion einen Fehler zurückgibt oder ein Timeout erreicht. Ohne verteilten Lock (oder wenn Sie ihn zu früh freigeben) kann ein Retry starten, während der ursprüngliche Lauf noch arbeitet.
Timeouts verschlimmern das. In Serverless kann die Plattform Ihren Code mitten in einer Aufgabe stoppen, wenn Sie das Zeitlimit erreichen. Vielleicht können Sie nicht aufräumen, und Sie wissen nicht, welche Schritte abgeschlossen sind. Wenn der Scheduler retryt, können Sie E-Mails doppelt senden, Karten doppelt belasten oder Duplikate schreiben.
Eine sicherere Methode ist, jeden Lauf fortsetzbar und idempotent zu machen. Denken Sie in Checkpoints, nicht in einer großen "mach alles"-Funktion. Zum Beispiel kann ein nächtlicher Rechnungsjob einen Fortschrittsspeicher wie "bis invoice_id 18420 verarbeitet" speichern und von dort weitermachen.
Guardrails, die verhindern, dass Retries und Backfills Schaden anrichten:
- Halten Sie den Lock für die gesamte Laufzeit. Geben Sie ihn erst frei, wenn Sie wirklich fertig sind.
- Protokollieren Sie eine run_id und Fortschrittsmarker, sodass ein Retry fortsetzen kann statt neu zu starten.
- Teilen Sie Arbeit in kleine Batches mit einer pro-Item "bereits verarbeitet"-Prüfung.
- Fügen Sie einen kontrollierten Backfill-Modus hinzu, der verpasste Fenster nacheinander abarbeitet.
Backfills sind wichtig, weil Zeitpläne ausrutschen. Wenn der gestrige Lauf fehlschlug, sollte der heutige Lauf nicht automatisch zwei Tage auf einmal verarbeiten, es sei denn Ihr System kann das sicher. Eine einfache Regel: "Bearbeite das älteste fehlende Fenster zuerst, dann stoppt."
Häufige Fehler und einfache Fixes
Die meisten Produktionsfehler entstehen durch wenige Entscheidungen, die im Prototyp gut erscheinen.
- TTL kürzer als der Job. Ihr Versprechen "ein Lauf" bricht, sobald ein Lauf langsam ist. Fix: TTL auf Worst-Case-Laufzeit plus Puffer setzen und während des Jobs erneuern.
- TTL zu lang. Ein abgestürzter Lauf kann den Schedule stundenlang blockieren. Fix: TTL vernünftig halten, im
finallyfreigeben und ein Owner-Token verwenden, damit eine andere Instanz nicht versehentlich entsperrt. - In-Memory-Locks. In Serverless landet jeder Lauf auf einer anderen Instanz — Speicherflags nützen nichts. Fix: einen gemeinsamen Store verwenden (DB-Zeile, Redis oder verwaltetes KV).
- "Exactly once" ohne Idempotenz annehmen. Retries und mindestens-einmal-Zustellung beißen Sie. Fix: mit einzigartigen Schlüsseln schreiben, Upserts nutzen oder vor Seiteneffekten Run-ID-Prüfungen durchführen.
- Logs als Heartbeat verwenden. Logs sind super zum Debuggen, aber mühsam zum Alerten. Fix: einen "last-ran"-Eintrag an einer abfragbaren Stelle schreiben (DB/KV/Metriken).
Eine leicht zu übersehende Ursache für stille Fehler ist Permission-Drift. Der Scheduler feuert noch, aber der Worker kann nach einer Änderung keine Secrets mehr lesen, nicht mehr in Storage schreiben oder keine API mehr aufrufen.
Schnelle Checkliste vor dem Produktions-Launch
Bevor Sie serverlose Cron-Jobs in Produktion vertrauen, machen Sie einen Pass, der sich auf langweilige Ausfälle konzentriert: ein ausgeschalteter Scheduler, ein zu früh ablaufender Lock oder ein Heartbeat, den niemand prüft.
- Bestätigen Sie, dass der Zeitplan in der richtigen Umgebung aktiviert ist und die Laufzeit-Identität Secrets lesen, in Ihre DB/Queue schreiben und Logs ausgeben kann.
- Schreiben Sie Ihre Concurrency-Guard auf: Lock-Key-Format, Speicherort, TTL und was bei Konflikt passiert (skip, reschedule oder fail).
- Validieren Sie die TTL mit realen Timings. Dauert ein Job manchmal 12 Minuten, führt ein 10-Minuten-Lock zu Überlappungen.
- Speichern Sie ein "last successful run"-Heartbeat an einem Ort, den Sie während eines Incidents schnell abfragen können, und inklusive Status (nicht nur ein Zeitstempel).
- Führen Sie zwei absichtliche Tests durch: (1) erzwingen Sie einen Fehler, um zu prüfen, dass Alerts einen Menschen erreichen, und (2) erzwingen Sie eine Überlappung, um sicherzustellen, dass der zweite Lauf blockiert wird und klar protokolliert.
Ein einfacher Überlappungs-Test: starten Sie einen Lauf mit einer absichtlichen Sleep/Wait in der Mitte und triggern Sie dann einen zweiten Lauf. Wenn Sie keine saubere "lock held, exiting"-Meldung sehen, ist Ihre Guard noch nicht zuverlässig.
Beispiel: ein nächtlicher Job, der niemals zweimal laufen darf
Ein häufiger, schmerzhafter Fall: Ein nächtlicher "Report-Export" läuft um 02:00, generiert PDFs und mailt sie an Kunden. Nach einem Deploy feuert der Scheduler zweimal (oder ein Retry greift) und einige Kunden erhalten doppelte E-Mails. Nichts ist "down", aber das Vertrauen sinkt schnell.
Die Lösung sind zwei kleine Bausteine: ein Lock, um Überlappungen zu verhindern, und ein Heartbeat, um stille Stops zu erkennen.
Zuerst holt sich der Job einen verteilten Lock (z. B. eine Datenbankzeile oder einen Key in einem verwalteten Store) mit einer TTL, die länger ist als die erwartete Laufzeit. Ist der Lock schon vergeben, beendet die zweite Invocation sich, bevor etwas gesendet wird.
Ein praktischer Ablauf:
- Versuchen, Lock "nightly-export" mit TTL 45 Minuten zu erwerben
- Falls Lock existiert, loggen "skipped: already running" und stoppen
- Falls Lock erworben, Export erstellen und E-Mails senden
- Lock freigeben (TTL ist Sicherheitsnetz, nicht der Plan)
Als Nächstes schreiben Sie einen Heartbeat wie last_success_at nachdem die E-Mails verschickt wurden. Dann prüfen Sie separat alle 15 Minuten und alarmieren, wenn now - last_success_at größer ist als 24 Stunden + ein Intervall. Das erkennt das Problem "Job hat nach Deploy aufgehört" schnell.
Für einen nicht-technischen Owner sind die besten Logs und Alerts in Klartext:
02:00:01 lock_acquired job=nightly-export run_id=abc123
02:07:44 completed job=nightly-export emails_sent=418 last_success_at=2026-01-20T02:07:44Z
02:00:02 skipped job=nightly-export reason=lock_held current_owner=abc123
ALERT: Nightly export has not succeeded in 25h. Last success: 2026-01-19 02:06 UTC. Check scheduler + secrets.
Nächste Schritte, wenn Ihre geplanten Jobs weiterhin unzuverlässig sind
Wenn Jobs trotz Lock und Heartbeat weiterhin überlappen oder "einfach aufhören", ist der Scheduler vielleicht in Ordnung, aber die Job-Logik ist fragil.
Ein typisches Zeichen: Die Cron-Logik stammt aus einem KI-generierten Prototyp. Dann sehen Sie oft einen Lock, der nicht wirklich geteilt wird, Secrets, die in Logs landen, und Retry-Verhalten, das zwar hilfreich aussieht, aber doppelte Seiteneffekte erzeugt.
Anzeichen, dass Sie im "Stop Patching"-Bereich sind:
- Fixes halten bis zum nächsten Deploy, dann ändern sich die Fehler wieder
- Niemand kann klar erklären, wann ein Lauf als "fertig" gilt
- Retries erzeugen doppelte E-Mails, Belastungen oder Writes
- Auth bricht unvorhersehbar (abgelaufene Tokens, fehlende Refresh, falsche Rollen)
- Logs erlauben nicht, einen Lauf End-to-End zu rekonstruieren
Dann lohnt sich meistens eine kurze Remediation-Phase statt weiterer Flickarbeit. Ziel ist kein Rewrite, sondern Vorhersagbarkeit: ein klarer Einstiegspunkt, eine Lock-Strategie, ein Satz Timeouts und ein Ort, an dem Erfolg protokolliert wird.
Wenn Sie mit einer kaputten KI-generierten Codebasis zu tun haben (besonders aus Tools wie Lovable, Bolt, v0, Cursor oder Replit), fokussiert FixMyMess bei fixmymess.ai die Diagnose und Reparatur von Problemen wie überlappenden Läufen, exponierten Secrets und fragiler Retry-Logik, damit der Job in Produktion zuverlässig läuft.