Zurück zu allen Beiträgen
·5 Min. Lesezeit

Zuverlässige Background Jobs: Idempotenz, Retries und Dead-Letter-Queues

E-Mails senden, PDFs erzeugen, Dateien scannen, einen Suchindex aktualisieren oder einen externen Anbieter benachrichtigen sollte oft nicht innerhalb einer HTTP-Anfrage passieren. Die Nutzeranfrage kann schnell enden; die Arbeit landet in einer Queue und ein Worker verarbeitet sie später. Messaging-Infrastruktur garantiert meist nicht, dass ein Job genau einmal läuft.

Ein Worker kann abstürzen, bevor die Bestätigung gesendet ist; die Queue stellt die Nachricht erneut zu. Ein Netzwerk-Timeout bedeutet, dass das Ergebnis unbekannt ist. Der Ausgangspunkt für zuverlässiges Design lautet: Ein Job kann erneut laufen, ungeordnet eintreffen und verzögert werden. Die Anwendung muss unter diesen Bedingungen ein korrektes Ergebnis erzeugen.

At-least-once-Zustellung akzeptieren

Viele Queue-Systeme verhalten sich ähnlich wie eine At-least-once-Zustellung. Damit eine Nachricht nicht verschwindet, warten sie auf eine explizite Bestätigung des Consumers. Trifft diese nicht ein, kann dieselbe Nachricht erneut zugestellt werden. Das ist kein Fehler, sondern eine Resilienz-Eigenschaft; der Handler muss doppelte Ausführung jedoch tolerieren.

Wird eine PaymentReceived-Nachricht zweimal verarbeitet, können zwei Kunden-E-Mails ärgerlich sein. Erhöht dieselbe Nachricht einen Kontostand zweimal, ist die Folge deutlich schwerwiegender. In beiden Fällen ist nicht die Queue das Problem. Der Consumer ist nicht sicher gegen wiederholte Verarbeitung.

Idempotenz in der Datenschicht verankern

Ein idempotenter Vorgang hinterlässt beim mehrfachen Aufruf mit derselben Eingabe dasselbe Zielergebnis. Eine nur im Speicher gehaltene Liste verarbeiteter Jobs ist nicht sicher: Sie verschwindet beim Neustart eines Workers. Die dauerhafte Datenschicht sollte die Quelle dieser Entscheidung sein.

Jede Nachricht oder Job-Anfrage braucht eine eindeutige Kennung. Ein Consumer kann diese Kennung in einer Tabelle verarbeiteter Operationen oder am Zielobjekt speichern. Trifft dieselbe Kennung erneut ein, gibt das System das erfolgreiche Ergebnis zurück, ohne die Nebenwirkung erneut zu erzeugen. Ein eindeutiger Datenbankindex ist eine wichtige Verteidigung, wenn zwei Worker dieselbe Nachricht gleichzeitig behandeln.

Auch der Gültigkeitsbereich eines Idempotency Keys ist wichtig. Sind zwei Klicks auf „Bericht neu erzeugen“ ein Job oder zwei getrennte Berichtsanfragen? Der Key muss sich an der geschäftlichen Bedeutung orientieren. Bei jedem Aufruf eine zufällige Request-ID zu erzeugen, unterscheidet keinen Retry; denselben Key für verschiedene Operationen zu verwenden, kann legitime Arbeit unterdrücken.

Retries nach Fehlertyp steuern

Nicht jeder Fehler verdient eine Wiederholung. Ein temporärer Netzwerkausfall, Überlastung, Timeout oder kurzer Fehler eines externen Dienstes kann erneut versucht werden. Ungültige Daten, verweigerte Berechtigung, beschädigte Datei oder eine Verletzung der Geschäftsregel werden durch Wiederholung nicht gültig. Ohne diese Unterscheidung füllt ein System die Queue und versteckt echte Probleme.

Für temporäre Fehler ist exponentielles Backoff üblich: kurzer Abstand beim ersten Retry, danach wachsende Intervalle. Jitter verhindert, dass viele Jobs gleichzeitig erneut versuchen und eine Abhängigkeit wieder überlasten. Auch maximale Anzahl der Versuche und Gesamtdauer brauchen Grenzen. Endlose Retries sind eine stille Schleife, die einen dauerhaften Fehler verbraucht.

Worker-Log oder Job-Datensatz sollten Anzahl der Versuche, letzte Fehlerkategorie und Zeitpunkt des nächsten Retrys zeigen. Der Support kann dann die Frage „Warum wartet diese Arbeit noch?“ mit einem konkreten Zustand statt einer allgemeinen Fehlermeldung beantworten.

Datenbankänderung und Message-Publishing nicht trennen

Ein häufiger Fehler schreibt zuerst einen Datenbankdatensatz und veröffentlicht eine Message in einem getrennten Schritt. Stürzt die Anwendung dazwischen ab, existiert der Datensatz, aber das Event wird nie veröffentlicht. Im umgekehrten Fall wird eine Message veröffentlicht, der Datensatz jedoch zurückgerollt; Consumer handeln dann für ein Objekt, das nie existierte.

Das Outbox-Pattern schließt diese Lücke. Die Geschäftsänderung und das zu veröffentlichende Event werden in derselben Datenbanktransaktion gespeichert. Ein separater Publisher liest Outbox-Datensätze, übermittelt sie an die Queue und markiert erfolgreiche Zustellung. Da auch der Publisher erneut laufen kann, behält die Message ihre eindeutige Kennung und Consumer bleiben idempotent.

Das erzeugt keine Exactly-once-Magie. Es macht verlorene Events und doppelte Zustellung explizit und beherrschbar.

Annahmen über Reihenfolge eng halten

Viele Job-Abläufe besitzen keine globale Nachrichtenreihenfolge. Zwei Events derselben Ressource können in unterschiedliche Partitionen gelangen, auf parallelen Workern laufen oder wegen eines Retrys in umgekehrter Reihenfolge ankommen. Die Annahme, dass OrderCreated immer vor OrderCancelled eintrifft, ist unsicher, sofern das System sie nicht ausdrücklich bewahrt.

Wo Reihenfolge nötig ist, sollte sie pro Geschäftsschlüssel und nicht im gesamten System erhalten werden. Events für dieselbe Bestellung oder dasselbe Dokument in eine Partition zu leiten, ist eine Option. In anderen Fällen kann ein Consumer die Eventversion prüfen und eine veraltete Aktualisierung ignorieren. Diese Entscheidungen hängen von der Domänenbedeutung ab; unnötige Reihenfolge beschränkt Kapazität.

Die Dead-Letter-Queue als Arbeitsfläche behandeln

Nachrichten, die die maximale Zahl an Versuchen überschreiten oder dauerhaft fehlschlagen, können in eine Dead-Letter-Queue (DLQ) verschoben werden. Diese Queue bedeutet nicht, dass das Problem verschwunden ist. Sie ist eine Fehlerarbeitsfläche, die menschliche oder automatisierte Prüfung benötigt.

Für jede fehlgeschlagene Nachricht sollten eindeutige Kennung, Fehlerkategorie, Zeitpunkt des ersten Auftretens, Versuchszahl und genug Geschäftskontext erhalten bleiben, um den Fehler ohne sensible Inhalte oder Zugangsdaten reproduzieren zu können. Kann der Betrieb die Nachricht sicher erneut abspielen, oder müssen Daten zuerst korrigiert werden? Dieser Weg muss vor einem Vorfall definiert sein.

Eine gesamte DLQ per Klick erneut zu senden, ist riskant. Ein dauerhafter Schemafehler oder ein Berechtigungsproblem kann Tausende Nachrichten erneut scheitern lassen. Gefiltertes, kontrolliertes und beobachtetes Replay ist sicherer.

Nutzerwirkung statt nur Queue-Tiefe messen

Eine leere Queue beweist nicht, dass ein Ablauf gesund ist. Eine notwendige E-Mail kann nie versendet worden sein, ein Suchindex nicht aktuell oder ein Dateiscan halbwegs stehen geblieben sein. Zu beobachten sind daher Abschlusszeit, Fehlerrate, Alter des ältesten wartenden Jobs, Retry-Rate und DLQ-Tiefe.

Für kritische Arbeit sollte auch ein nutzersichtbarer Status gestaltet werden: „wird verarbeitet“, „abgeschlossen“, „wird erneut versucht“ oder „an Support weitergegeben“ senkt Unsicherheit. Eine definitive Erfolgsmeldung ist zu vermeiden, solange der Job nicht tatsächlich abgeschlossen ist.

Zuverlässige Background Jobs sollen Fehler nicht beseitigen. Sie sollen sich von doppelter Zustellung, Teilausfällen und Abhängigkeitsfehlern erholen, ohne falsche oder irreversible Nebenwirkungen zu erzeugen. Idempotenz, selektive Retries, Outbox und eine betreibbare Fehlerqueue bilden das Fundament dieser Resilienz.