Blogs / Celery Retries, Backoff und Dead-Letter-Queues

Celery Retries, Backoff und Dead-Letter-Queues

Veröffentlicht
12. September 2026
Autor
Faizan Nadeem
Tags
Celery Python Task Queues Reliability
Ein altmodischer Wecker mit zwei Glocken und römischen Ziffern vor weißem Hintergrund
Foto von insung yoon auf Unsplash

Das Standard-Retry-Verhalten, mit dem die meisten Teams Celery ausliefern, ist max_retries=3 und sonst nichts — kein Backoff, kein Jitter, keine Unterscheidung zwischen „die nachgelagerte API ist für zehn Sekunden down" und „dieser Task wird niemals erfolgreich sein, egal wie oft man ihn ausführt". Dann hat eine Abhängigkeit einen schlechten Nachmittag, jeder Task, der sie trifft, schlägt fehl, jeder dieser Retries feuert innerhalb derselben Sekunde erneut, und der Retry-Storm vollendet, was der ursprüngliche Ausfall begonnen hat. Die Retries haben nicht geholfen. Sie haben das Problem zu dem verstärkt, was den Incident tatsächlich verursacht hat.

Der Fix ist nicht „weniger retrien". Er besteht darin, mit Absicht zu retrien: wissen, welche Fehler überhaupt einen Retry wert sind, diese Retries so verteilen, dass sie nicht alle im selben Moment landen, und — entscheidend — einen Ort haben, an den ein Task gehen kann, sobald er jeden vernünftigen Versuch ausgeschöpft hat, statt einfach aus einer Log-Zeile zu verschwinden, die niemand beobachtet hat. Genau dieser letzte Teil ist es, den die meisten Celery-Setups komplett auslassen, weil Celery einem keine Dead-Letter-Queue gratis mitgibt, so wie RabbitMQ-native Consumer eine bekommen.

Kurz gesagt: Exponential Backoff verteilt Retries mit wachsender Verzögerung nach jedem Fehler, statt sie alle auf einmal abzufeuern, und eine Dead-Letter-Queue ist der dauerhafte Ort, an den ein Task geht, sobald er alle Retries ausgeschöpft hat, damit er inspiziert und reprocessed werden kann, statt einfach zu verschwinden.

Das lernst du:

  • Wie man transiente Fehler, die einen Retry wert sind, von permanenten unterscheidet, die es nie sein sollten
  • Wie man autoretry_for korrekt einsetzt, inklusive der Fehler, die dazu führen, dass es die falschen Dinge retriet
  • Wie man Celery Retry Exponential Backoff mit Jitter konfiguriert, damit sich Retries nicht zu einer Thundering Herd aufstauen
  • Warum max_retries einen bewussten Wert braucht, nicht einfach Celerys Default
  • Wie man eine funktionierende Celery Dead-Letter-Queue baut, da Celery keine eingebaute hat
  • Wie man Tasks, die in der Dead-Letter-Queue landen, routet, Alerts dafür einrichtet und sicher reprocessed

Inhaltsverzeichnis

  1. Transiente vs. permanente Fehler
  2. Retries mit autoretry_for
  3. Exponential Backoff und Jitter
  4. Sauber aufgeben mit max_retries
  5. Globale Defaults vs. Per-Task-Overrides setzen
  6. Eine Dead-Letter-Queue in Celery bauen
  7. Alerting und Reprocessing aus der Dead-Letter-Queue
  8. Häufige Fallstricke
  9. Häufige Fragen

Transiente vs. permanente Fehler

Jede Retry-Strategie beginnt mit einer Frage, die der Code der meisten Tasks nie wirklich beantwortet: Ist dieser Fehler die Art, die verschwindet, wenn man wartet, oder die Art, die in einer Stunde immer noch da sein wird? Die beiden zu vermischen ist die mit Abstand größte Ursache für schlechtes Retry-Verhalten.

Transiente Fehler sind solche, bei denen exakt derselbe Input, etwas später erneut versucht, plausibel erfolgreich ist: ein für einen Moment erschöpfter Datenbank-Connection-Pool, eine nachgelagerte HTTP-API, die einen 503 zurückgibt, ein Netzwerk-Timeout, ein kurzer Redis-Connection-Aussetzer. Diese sind einen Retry wert, und zwar mit steigender Verzögerung, weil die zugrunde liegende Bedingung oft last- oder zeitbasiert ist und sich meist von selbst löst.

Permanente Fehler werden durch Warten nicht behoben: ein ValidationError, weil der Input fehlerhaft war, ein 404 von einer API, weil die Ressource schlicht nicht existiert, ein KeyError durch einen Programmierfehler im Task selbst. Diese zu retrien verschwendet nicht nur Worker-Kapazität — es verzögert auch den Punkt, an dem ein Mensch tatsächlich mitbekommt, dass etwas kaputt ist, weil der Task für die Dauer des Retry-Schedules wie „noch in Arbeit" aussieht, statt sofort als fehlgeschlagen aufzufallen.

Die praktische Konsequenz ist, dass autoretry_for fast nie ein blankes Exception sein sollte. Es sollte ein spezifisches, bewusst gewähltes Tupel von Exception-Typen sein, von denen man entschieden hat, dass sie für diesen konkreten Task tatsächlich transient sind.

import requests
from celery import shared_task

class PermanentFailure(Exception):
    """Raised for errors that will never succeed on retry."""

@shared_task(bind=True)
def sync_customer_record(self, customer_id: int):
    response = requests.get(f"https://crm.example.com/customers/{customer_id}", timeout=5)

    if response.status_code == 404:
        # The customer genuinely doesn't exist upstream — retrying won't change that.
        raise PermanentFailure(f"customer {customer_id} not found in CRM")

    response.raise_for_status()  # raises for 5xx/connection errors — worth retrying
    return response.json()

Bewusst einen eigenen Exception-Typ für den „das hier nicht retrien"-Fall zu werfen, statt jede Exception in denselben Topf fallen zu lassen, ist das, was die autoretry_for-Konfiguration im nächsten Abschnitt überhaupt erst sinnvoll macht.

Eine kurze Referenz: Was ist wirklich transient

Fehler zu klassifizieren wird einfacher mit einer konkreten Referenztabelle, statt für jeden Task neu darüber nachzudenken:

FehlerMeist transient?Anmerkungen
Connection Timeout / RefusedJaRetry mit Backoff — die Abhängigkeit steht evtl. unter Last oder startet gerade neu
HTTP 503 / 429JaExplizite „Zurückfahren"-Signale; einen vorhandenen Retry-After-Header respektieren
HTTP 500ManchmalHängt davon ab, ob es ein transienter Serverfehler oder ein durchgängiger Bug auf der Gegenseite ist — Retries niedrig deckeln und eskalieren, wenn es sich wiederholt
HTTP 404 / 400NeinDie Ressource oder der Request selbst ist falsch; ein Retry ändert den Input nicht
IntegrityError (Unique Constraint)NeinDer Datenkonflikt ist beim Retry immer noch da — braucht Anwendungslogik, keinen Retry
Datenbank-DeadlockJaVon Natur aus tatsächlich transient; kurzer Backoff, keine lange Verzögerung nötig
KeyError / TypeError aus Task-CodeNeinDas ist ein Bug, kein Umgebungsfehler — ein Retry verzögert nur die Bug-Meldung
Redis ConnectionErrorJaBroker- oder Cache-Aussetzer; retrien, aber untersuchen, wenn es häufig wiederkehrt

Das ist als Ausgangspunkt gedacht, nicht als Universalregel — die richtige Klassifizierung hängt davon ab, was die eigene, konkrete Abhängigkeit tatsächlich tut, wenn sie beeinträchtigt ist, und es lohnt sich, sie nach jedem Incident zu überarbeiten, der einen Fehlermodus zutage fördert, den man nicht auf dem Schirm hatte.

Retries mit autoretry_for

autoretry_for ist Celerys deklarativer Retry-Mechanismus — statt Exceptions manuell abzufangen und self.retry() aufzurufen, listet man die Exception-Typen auf, die einen automatischen Retry auslösen sollen, und Celery übernimmt den Rest.

from celery import shared_task
from requests.exceptions import ConnectionError, Timeout, HTTPError

@shared_task(
    bind=True,
    autoretry_for=(ConnectionError, Timeout, HTTPError),
    retry_backoff=True,
    retry_backoff_max=600,
    retry_jitter=True,
    max_retries=5,
)
def sync_customer_record(self, customer_id: int):
    response = requests.get(f"https://crm.example.com/customers/{customer_id}", timeout=5)
    response.raise_for_status()
    return response.json()

Die zwei Fehler, die bei autoretry_for ständig auftauchen: Exceptions zu breit auflisten (fängt man Exception selbst ab, wird ein echter Bug im Task-Code — ein TypeError durch ein misslungenes Refactoring — still fünfmal retried, statt beim ersten Lauf laut zu scheitern, und ein echter Defekt verschwindet in dem, was wie normales Retry-Rauschen aussieht), und vergessen, dass autoretry_for den gesamten Task-Body von vorn retriet, nicht nur den Teil, der fehlgeschlagen ist. Führt der eigene Task nacheinander zwei Seiteneffekte aus und der zweite wirft eine Exception, führt der Retry auch den ersten erneut aus — das ist genau das Problem doppelter Ausführung, zu dessen Lösung idempotentes Task-Design existiert, und es lohnt sich, von Anfang an dafür zu designen, statt es erst mitten in einem Incident zu entdecken.

HTTPError verdient eine gesonderte Erwähnung: response.raise_for_status() wirft ihn sowohl bei einem 503 (transient, einen Retry wert) als auch bei einem 404 (permanent, keinen Retry wert) — derselbe Exception-Typ deckt beide Fälle ab. HTTPError pauschal zu retrien bedeutet, 404er fünfmal zu retrien, bevor man aufgibt, was verschwendete Arbeit und ein verzögertes Fehlersignal ist. Das PermanentFailure-Muster aus dem vorherigen Abschnitt — den Status-Code prüfen und für die permanenten Fälle eine eigene, nicht retriete Exception werfen — ist das, was das tatsächlich behebt; autoretry_for allein kann nicht zwischen zwei Fehlern unterscheiden, die sich eine Exception-Klasse teilen.

Exponential Backoff und Jitter

Sofort nach einem Fehler erneut zu versuchen ist für einen transienten, lastbedingten Fehler nahezu die schlechteste Option — es feuert erneut, während das nachgelagerte System noch unter genau dem Druck steht, der den ersten Fehler verursacht hat. Exponential Backoff verteilt Retries mit steigender Verzögerung: 2 Sekunden, dann 4, dann 8, dann 16, und gibt der ausfallenden Abhängigkeit so Raum, sich zwischen den Versuchen tatsächlich zu erholen.

@shared_task(
    bind=True,
    autoretry_for=(ConnectionError, Timeout),
    retry_backoff=True,       # enables exponential backoff
    retry_backoff_max=600,    # cap the delay at 10 minutes
    retry_jitter=True,        # randomize the delay to avoid a thundering herd
    max_retries=6,
)
def call_flaky_api(self, payload: dict):
    return flaky_api_client.post(payload)

retry_backoff=True nutzt Celerys eingebaute Formel — standardmäßig ungefähr 2 ** retry_count Sekunden, gedeckelt bei retry_backoff_max — sodass der Schedule für einen Task mit max_retries=6 in etwa wie 1s, 2s, 4s, 8s, 16s, 32s aussieht (jeweils beim Max gedeckelt), statt fünf Retries direkt hintereinander abzufeuern.

retry_jitter ist das Detail, das man leicht auslässt und dessen Auslassen in der Praxis teuer wird. Ohne es berechnet jede Task-Instanz, die zum selben Zeitpunkt fehlgeschlagen ist — etwa weil eine gemeinsam genutzte, nachgelagerte Abhängigkeit einen zweisekündigen Ausfall hatte —, exakt dieselbe Backoff-Verzögerung und retriet zum exakt selben Zeitpunkt, nur um einen festen Offset verschoben. Hunderte oder Tausende Tasks, die alle im selben 100-ms-Fenster retrien, sind eine selbst verursachte Thundering Herd, und das kann einen kurzen Aussetzer in einen anhaltenden Ausfall verwandeln, weil die Retry-Spitze selbst den sich erholenden Service überlastet. Jitter randomisiert die tatsächliche Verzögerung jedes Tasks innerhalb seines Backoff-Fensters und verteilt dieselbe Herde über Sekunden, statt sie in einem einzigen Moment zu konzentrieren.

Für manuelle Retry-Logik (wenn autoretry_for nicht passt, z. B. weil man die Exception inspizieren muss, um die Verzögerung zu bestimmen), dieselbe Formel von Hand angewendet:

import random

@shared_task(bind=True, max_retries=6)
def call_flaky_api_manual(self, payload: dict):
    try:
        return flaky_api_client.post(payload)
    except (ConnectionError, Timeout) as exc:
        backoff = min(600, 2 ** self.request.retries)
        jitter = random.uniform(0, backoff * 0.5)
        raise self.retry(exc=exc, countdown=backoff + jitter)

Sauber aufgeben mit max_retries

Celerys Default für max_retries ist 3, still angewendet, wenn man ihn nie setzt — ein Wert, der aus keinem Grund gewählt wurde, der etwas mit den tatsächlichen Fehlereigenschaften des eigenen Tasks zu tun hat. Die richtige Zahl hängt davon ab, wie lange der Fehlermodus, gegen den man sich absichert, typischerweise anhält: einen Datenbank-Connection-Aussetzer fünfmal über 30 Sekunden zu retrien ist vernünftig; einen Ausfall einer externen API für dieselben 30 Sekunden zu retrien, wenn die Ausfälle dieses Providers typischerweise 20 Minuten dauern, garantiert nur, dass man die Retries ausschöpft, bevor sich die Abhängigkeit erholt — und das leise, einen Task nach dem anderen, ohne dass jemand das Muster bemerkt.

max_retries bewusst setzen und — das ist der Teil, den man leicht auslässt — explizit entscheiden, was passiert, wenn er ausgeschöpft ist:

from celery.exceptions import MaxRetriesExceededError

@shared_task(bind=True, max_retries=5)
def call_flaky_api(self, payload: dict):
    try:
        return flaky_api_client.post(payload)
    except (ConnectionError, Timeout) as exc:
        try:
            backoff = min(600, 2 ** self.request.retries)
            raise self.retry(exc=exc, countdown=backoff)
        except MaxRetriesExceededError:
            send_to_dead_letter_queue(task_name=self.name, args=[payload], error=str(exc))
            raise

Einen Task nach seinem letzten Retry einfach fehlschlagen zu lassen, ohne dass diesem Fehlschlag irgendetwas nachgeschaltet ist, ist das Default-Verhalten, das zu der „das haben wir drei Tage lang nicht bemerkt"-Klasse von Incidents führt — der Task taucht als roter Eintrag in Flower oder als Log-Zeile auf, und solange niemand diese Oberfläche aktiv beobachtet, ist er praktisch unsichtbar. Genau diese Lücke schließt eine Dead-Letter-Queue.

Globale Defaults vs. Per-Task-Overrides setzen

retry_backoff=True, retry_jitter=True, retry_backoff_max=600 bei jeder einzelnen Task-Definition zu wiederholen, vergisst man leicht ausgerechnet bei dem einen Task, auf den es am meisten ankommt. Celery erlaubt es, vernünftige Defaults global zu setzen und nur dort zu überschreiben, wo ein konkreter Task tatsächlich anderes Verhalten braucht:

# celeryconfig.py
task_annotations = {
    "*": {
        "retry_backoff": True,
        "retry_backoff_max": 600,
        "retry_jitter": True,
        "max_retries": 5,
    },
    "myapp.tasks.charge_customer": {
        "max_retries": 2,          # payment retries should stay conservative
        "retry_backoff_max": 60,
    },
    "myapp.tasks.sync_analytics_batch": {
        "max_retries": 10,         # low-stakes, safe to retry aggressively
        "retry_backoff_max": 1800,
    },
}

task_annotations wendet den Wildcard-Eintrag auf jeden Task an und legt dann task-spezifische Overrides nach Namen darüber. Das lohnt sich, früh einzurichten, gerade weil der Fehlermodus, keinen globalen Default zu haben, kein Fehler ist — sondern Stille. Ein neu hinzugefügter Task, der vergisst, Backoff und Jitter zu setzen, retriet einfach still und sofort, sechsmal hintereinander, und niemand bemerkt es, bis er der Task ist, der beim nächsten echten Ausfall zu einem Retry-Storm beiträgt. Ein globaler Default macht sicheres Verhalten zum Weg des geringsten Widerstands, statt zu etwas, das sich jeder Task-Autor merken muss.

Eine Dead-Letter-Queue in Celery bauen

Anders als bei RabbitMQ-Consumern, die AMQP direkt nutzen, liefert Celery keine erstklassige Dead-Letter-Queue von Haus aus mit — es gibt kein dead_letter_exchange-Config-Flag, das man umlegt. Man baut sie selbst, und ihre Form ist einfacher, als es klingt: ein durabler Ort, um den vollständigen Kontext eines Tasks zu schreiben, der seine Retries ausgeschöpft hat, damit er inspiziert, mit einem Alert versehen und reprocessed werden kann, statt im Nichts der ausgeschöpften Retries zu verschwinden.

Die einfachste Version ist eine Datenbanktabelle:

class DeadLetterTask(db.Model):
    __tablename__ = "dead_letter_tasks"
    id = db.Column(db.Integer, primary_key=True)
    task_name = db.Column(db.String(255), nullable=False)
    task_args = db.Column(db.JSON, nullable=False)
    task_kwargs = db.Column(db.JSON, nullable=False)
    error_message = db.Column(db.Text, nullable=False)
    retry_count = db.Column(db.Integer, nullable=False)
    failed_at = db.Column(db.DateTime, default=datetime.utcnow)
    reprocessed = db.Column(db.Boolean, default=False)
from celery.signals import task_failure

@task_failure.connect
def handle_task_failure(sender=None, task_id=None, exception=None, args=None,
                         kwargs=None, einfo=None, **extra):
    # Only trap the final failure — Celery fires this signal once retries
    # are genuinely exhausted, not on every intermediate retry attempt.
    db.session.add(DeadLetterTask(
        task_name=sender.name,
        task_args=list(args or []),
        task_kwargs=dict(kwargs or {}),
        error_message=str(exception),
        retry_count=sender.request.retries if sender.request else 0,
    ))
    db.session.commit()

Sich global in das task_failure-Signal einzuhängen, statt jeden Task einzeln zu wrappen, bedeutet, dass neue Tasks automatisch Dead-Letter-Abdeckung bekommen, ohne dass jeder sein eigenes Boilerplate braucht — der Trade-off ist, dass es beim finalen Fehlschlag jedes Tasks feuert, weshalb man in der Regel einen Weg braucht, bestimmte Low-Stakes-Tasks davon auszunehmen, statt jeden Fehlschlag unterschiedslos in dieselbe Tabelle zu routen.

Läuft RabbitMQ statt Redis als Broker, gibt es eine tiefer angesiedelte Alternative: einen eigenen Dead-Letter-Exchange und eine eigene Queue auf Broker-Ebene konfigurieren und x-dead-letter-exchange auf den eigenen Task-Queues setzen, damit RabbitMQ selbst zurückgewiesene oder abgelaufene Nachrichten dorthin routet. Das ist robuster als der Signal-Ansatz auf Anwendungsebene, weil es sogar überlebt, wenn der eigene Celery-Worker-Prozess abstürzt, bevor der task_failure-Handler läuft — es erfordert aber Konfiguration auf Broker-Ebene außerhalb von Celerys eigenen Settings und steht bei Redis generell gar nicht zur Verfügung, was ein weiterer Grund ist, warum es sich lohnt, die in dem Task-Queue-Vergleich besprochene Broker-Wahl frühzeitig richtig zu treffen, bevor man tief in der Produktions-Reliability-Arbeit steckt.

Alerting und Reprocessing aus der Dead-Letter-Queue

Eine Dead-Letter-Queue, die sich niemand ansieht, ist kaum besser als keine Dead-Letter-Queue — sie wandelt nur „still verschwunden" in „still in einer Tabelle angesammelt" um. Zwei Dinge machen sie tatsächlich nützlich:

Alerting. Eine Benachrichtigung in dem Moment auslösen, in dem etwas im Dead-Letter-Store landet, nicht nach einem Zeitplan — eine tägliche Digest-E-Mail ist in Ordnung, um ein Gefühl fürs Volumen zu bekommen, aber das erste Auftreten eines neuen Fehlermusters verdient einen sofortigen Page- oder Slack-Alert, weil es oft das früheste Signal für einen Ausfall in einer Abhängigkeit ist, die man nicht direkt überwacht. Jenseits einer Handvoll solcher Alerts tauchen dieselben Fan-out- und Zustellgarantie-Fragen auf, die auch produktive Benachrichtigungssysteme im großen Maßstab lösen müssen.

@task_failure.connect
def alert_on_task_failure(sender=None, task_id=None, exception=None, **extra):
    if is_novel_failure(sender.name, str(exception)):
        notify_slack(f"Task {sender.name} exhausted retries: {exception}")

Reprocessing. Sobald die zugrunde liegende Ursache behoben ist — die nachgelagerte API ist wieder da, die fehlerhaften Daten wurden korrigiert —, sollte das Reprocessing dead-letterter Tasks eine bewusste, auditierbare Aktion sein, keine automatische Retry-Loop, die man wieder anschraubt. Ein kleines Skript, das ausgewählte Zeilen erneut einreiht und sie als reprocessed=True markiert, hält eine klare Aufzeichnung darüber, was wann erneut versucht wurde — das ist wichtig, wenn derselbe Fehler wiederkehrt und man wissen muss, ob es eine neue Instanz ist oder der alte Fehler zurückkommt.

def reprocess_dead_letter_task(dead_letter_id: int):
    row = DeadLetterTask.query.get(dead_letter_id)
    task = celery_app.tasks[row.task_name]
    task.apply_async(args=row.task_args, kwargs=row.task_kwargs)
    row.reprocessed = True
    db.session.commit()

Der Versuchung widerstehen, alles in der Dead-Letter-Queue automatisch per Cron-Job zu reprocessen — das führt still genau das Retry-Storm-Risiko wieder ein, das die Backoff- und Jitter-Einstellungen verhindern sollten, nur auf einem längeren Zeithorizont, und es entfernt den menschlichen Entscheidungspunkt, den Dead-Lettering überhaupt erst schaffen sollte.

Häufige Fallstricke

Fehler: Exception breit retrien statt spezifischer transienter Fehlertypen. Das retriet echte Bugs genauso wie Netzwerkaussetzer und versteckt echte Defekte in normal aussehendem Retry-Rauschen. Lösung: die spezifischen Exceptions auflisten, die für diesen Task tatsächlich transient sind, und alles andere sofort fehlschlagen lassen.

Fehler: kein Jitter beim Backoff. Ein Batch von Tasks, die gemeinsam fehlgeschlagen sind, retriet auch gemeinsam und macht aus einem kurzen Ausfall einen selbst verursachten Retry-Storm. Lösung: immer retry_jitter=True zusammen mit retry_backoff=True setzen.

Fehler: annehmen, autoretry_for retriet nur die fehlgeschlagene Zeile. Es führt den gesamten Task-Body von vorn erneut aus. Lösung: Tasks so entwerfen, dass sie vollständig sicher erneut ausführbar sind, mithilfe der separat behandelten Idempotenz-Muster, statt anzunehmen, dass Teilfortschritt einen Retry übersteht.

Fehler: gar keine Dead-Letter-Queue, nur Logs. Fehlschläge mit ausgeschöpften Retries werden unsichtbar, solange nicht jemand aktiv ein Dashboard beobachtet. Lösung: sich in task_failure einhängen (oder das native Dead-Lettering des eigenen Brokers nutzen), um jeden ausgeschöpften Fehlschlag irgendwo abfragbar und alertbar zu persistieren.

Fehler: die Dead-Letter-Queue automatisch nach einem Timer reprocessen. Das führt still das Retry-Storm-Risiko wieder ein, das Backoff eigentlich verhindern sollte, nur mit längerem Zyklus. Lösung: Reprocessing zu einer bewussten, protokollierten Aktion machen, die erst erfolgt, nachdem bestätigt wurde, dass die zugrunde liegende Ursache tatsächlich behoben ist.

Retries und Dead-Letter-Queues sind keine separaten Anliegen, die man an eine Task-Queue anschraubt — sie sind die zwei Enden derselben Design-Entscheidung darüber, was passiert, wenn etwas schiefgeht. Die Fehler retrien, die einen Retry wert sind, sie weit genug verteilen, damit sie das Problem nicht verschärfen, und laut und sichtbar aufgeben statt leise, in eine Tabelle, die niemand abfragt. Das ist der Unterschied zwischen einer Queue, die unter einem schlechten Nachmittag graceful degradiert, und einer, die einen zehnsekündigen Ausfall zum eigentlichen Incident macht.

Häufige Fragen

Hat Celery eine eingebaute Dead-Letter-Queue? Nein. Anders als bei RabbitMQ-nativen Consumern liefert Celery keine mit — man baut sie selbst, typischerweise indem man sich in das task_failure-Signal einhängt, um Tasks mit ausgeschöpften Retries irgendwo abfragbar und alertbar zu persistieren.

Was ist ein guter Standardwert für max_retries in Celery? Es gibt keine universelle Zahl — Celerys eigener Default von 3 ist willkürlich gewählt. Den Wert danach setzen, wie lange der Fehlermodus, gegen den man sich absichert, typischerweise anhält: eine Handvoll Retries über 30 Sekunden für einen Datenbank-Blip, mehr und länger für eine Abhängigkeit mit 20-minütigen Ausfällen.

Sollte retry_jitter immer zusammen mit retry_backoff aktiviert sein? Ja. Ohne Jitter berechnet jeder Task, der zum selben Zeitpunkt fehlgeschlagen ist, dieselbe Verzögerung und retriet im selben Moment — das macht aus einem kurzen Ausfall einen selbstverschuldeten Retry-Storm.

Retriet autoretry_for nur den fehlgeschlagenen Teil eines Tasks? Nein — es führt den gesamten Task-Body von vorn erneut aus, inklusive aller Seiteneffekte, die bereits erfolgreich waren. Deshalb müssen Tasks so entworfen werden, dass sie vollständig sicher erneut ausführbar sind, statt nur teilweise wiederholt zu werden.

Faizan Nadeem
Autor
Faizan Nadeem

AI/GenAI engineer specialising in agentic RAG, LangGraph, and production LLM systems with Python and FastAPI. 2 years building AI agents, RAG pipelines, and the backends behind them.

Weitere Artikel