Bootcamp backendontwikkeling met FastAPI · Les

Retries, idempotentie en dead-letterafhandeling

Maak taken robuust met exponentiële backoff, idempotentiesleutels en dead-letter-routing voor onbruikbare berichten.

Les 3 van 413 stappen

Retries, idempotentie en dead-letterafhandeling is een gratis Bootcamp backendontwikkeling met FastAPI-les op CoddyKit. Dit is les 3 van 4. Je kunt 3 lessen uit dit leerpad gratis volledig lezen — daarna ontgrendelt CoddyKit PRO alle lessen, plus praktische oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Bootcamp backendontwikkeling met FastAPI. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Bootcamp backendontwikkeling met FastAPI bevat in totaal 4 lessen.

Waarom taken veerkracht nodig hebben

In een FastAPI-backend stuur je traag werk (e-mails versturen, betalingen uitvoeren, API's van derden aanroepen) naar een Celery-worker, zodat het HTTP-verzoek snel terugkeert. Maar achtergrondwerk wordt uitgevoerd in een vijandige omgeving: netwerken haperen, API's beperken je snelheid en workers crashen midden in een taak.

Een veerkrachtige taak moet drie foutscenario's overleven:

  • Tijdelijke fouten — probeer het opnieuw met exponentiële wachttijd, zodat je een haperende service niet blijft overbelasten.
  • Dubbele aflevering — hetzelfde bericht kan twee keer worden verwerkt, dus taken moeten idempotent zijn.
  • Vergiftigde berichten — een taak die voor altijd blijft mislukken moet naar een dead-letterwachtrij worden gerouteerd in plaats van eindeloos te blijven herhalen.

In deze les verbind je alle drie.

Levering minstens één keer

Celery-brokers (RabbitMQ, Redis) bieden levering minstens één keer, niet precies één keer. Een bericht wordt pas bevestigd (ack) nadat de taak is voltooid. Als een worker stopt nadat het werk is uitgevoerd maar voordat het bericht is bevestigd, levert de broker het bericht opnieuw af en wordt de taak opnieuw uitgevoerd.

Met acks_late=True gebeurt de bevestiging na de uitvoering — veiliger bij crashes, maar dit garandeert dat sommige taken twee keer worden uitgevoerd. Daarom is idempotentie niet optioneel.

from celery import Celery

app = Celery("jobs", broker="redis://localhost:6379/0")

# Recommended resilience defaults
app.conf.update(
    task_acks_late=True,          # ack only after the task body returns
    task_reject_on_worker_lost=True,  # requeue if the worker is killed
    worker_prefetch_multiplier=1, # don't hoard messages on one worker
)

Automatisch opnieuw proberen met autoretry_for

De eenvoudigste manier om opnieuw te proberen is declareren welke uitzonderingen opnieuw geprobeerd mogen worden. Celery vangt ze op en plant de taak automatisch opnieuw in.

  • autoretry_for — de uitzonderingsklassen die een nieuwe poging activeren.
  • max_retries — de limiet voordat de taak als mislukt wordt gemarkeerd.
  • retry_backoff — schakelt exponentiële wachttijd in (de wachttijden verdubbelen bij elke poging).

Probeer alleen opnieuw bij tijdelijke fouten (time-outs, 5xx-fouten, verbroken verbindingen). Probeer nooit blind opnieuw bij een ValueError door ongeldige invoer — die mislukt elke keer op dezelfde manier.

import requests
from celery import Celery

app = Celery("jobs", broker="redis://localhost:6379/0")

@app.task(
    autoretry_for=(requests.exceptions.RequestException,),
    max_retries=5,
    retry_backoff=True,        # 1s, 2s, 4s, 8s, ...
    retry_backoff_max=600,     # cap the delay at 10 minutes
    retry_jitter=True,         # randomize to avoid thundering herd
)
def call_payment_api(charge_id: str):
    resp = requests.post("https://api.example.com/charge", json={"id": charge_id}, timeout=10)
    resp.raise_for_status()
    return resp.json()

Exponentiële wachttijd uitgelegd

Exponentiële wachttijd betekent dat de tijd tussen pogingen meetkundig toeneemt: de vertraging voor poging n is ongeveer base * 2 ** n, met een maximum. Zo krijgt een haperende achterliggende service tijd om te herstellen, in plaats van steeds opnieuw te worden overbelast.

Jitter voegt willekeur toe, zodat duizenden taken die op hetzelfde moment zijn mislukt niet allemaal op hetzelfde moment opnieuw worden geprobeerd (de zogenaamde “stormloop”). Hier zie je de berekening die Celery toepast, als een eenvoudig zelfstandig programma.

import random

def backoff_delay(attempt, base=1, cap=600, jitter=True):
    delay = min(cap, base * (2 ** attempt))
    if jitter:
        delay = random.uniform(0, delay)  # full jitter
    return delay

for attempt in range(8):
    raw = min(600, 1 * (2 ** attempt))
    print(f"attempt {attempt}: raw={raw:>3}s  jittered~={backoff_delay(attempt):6.1f}s")

Handmatig opnieuw proberen met self.retry

Wanneer je aangepaste logica nodig hebt — bijvoorbeeld de wachttijd bepalen op basis van een responseheader of alleen opnieuw proberen bij bepaalde statuscodes — bind je de taak en roep je expliciet self.retry() aan.

Gebruik bind=True om self te krijgen, lees self.request.retries om het nummer van de poging te kennen en geef countdown op voor de wachttijd. Door het resultaat van self.retry() te raisen, stop je de huidige uitvoering netjes.

import requests
from celery import Celery

app = Celery("jobs", broker="redis://localhost:6379/0")

@app.task(bind=True, max_retries=5)
def sync_inventory(self, sku: str):
    resp = requests.get(f"https://api.example.com/stock/{sku}", timeout=5)
    if resp.status_code == 429:  # rate limited
        wait = int(resp.headers.get("Retry-After", 2 ** self.request.retries))
        raise self.retry(countdown=wait)
    resp.raise_for_status()
    return resp.json()["qty"]

Wat idempotentie echt betekent

Een bewerking is idempotent als twee keer uitvoeren hetzelfde effect heeft als één keer uitvoeren. Omdat Celery berichten ten minste één keer aflevert, moet elke taak die de status wijzigt (een kaart belasten, een record aanmaken, de voorraad verlagen) idempotent zijn. Anders belast je klanten dubbel.

Het standaardhulpmiddel is een idempotentiesleutel: een unieke identificatie voor de zakelijke intentie, niet voor het bericht. Je legt in permanente opslag vast: “ik heb sleutel X al verwerkt” en slaat de volgende aflevering dan over.

  • Geef de sleutel mee vanuit het API-verzoek (clients kunnen deze ook aanleveren).
  • Sla de sleutel op in een tabel of Redis met een UNIQUE-beperking.
  • De beperking — niet de applicatielogica — zorgt ervoor dat dit racebestendig is.

Een idempotentiebewaker

Hier zie je het patroon op zichzelf: een bewaker die voltooide sleutels onthoudt en weigert het effect twee keer uit te voeren, zelfs bij gelijktijdige aanroepen. In echte code wordt de set seen een Redis-SET NX of een database-rij met een unieke sleutel, maar de logica blijft hetzelfde.

import threading

class IdempotencyGuard:
    def __init__(self):
        self._seen = set()
        self._lock = threading.Lock()

    def run_once(self, key, effect):
        with self._lock:            # the unique-constraint stand-in
            if key in self._seen:
                return "skipped (duplicate)"
            self._seen.add(key)
        return effect()

guard = IdempotencyGuard()
charges = []

def charge():
    charges.append(99)
    return "charged 99"

print(guard.run_once("order-123", charge))
print(guard.run_once("order-123", charge))  # duplicate delivery
print("total charges applied:", len(charges))

Idempotentie binnen een Celery-taak

In de praktijk wikkel je het effect in een databasetransactie en laat je een UNIQUE-beperking de bron van waarheid zijn. Voeg eerst de idempotentiesleutel in; als het invoegen een fout wegens een unieke schending veroorzaakt, heeft een eerdere (of gelijktijdige) aflevering dit al afgehandeld en keer je meteen terug.

Zo blijven de controle en het neveneffect atomair — er is geen moment waarop de ene aflevering “niet uitgevoerd” ziet terwijl een andere bezig is met het belasten van de kaart.

from sqlalchemy.exc import IntegrityError
from celery import Celery

app = Celery("jobs", broker="redis://localhost:6379/0")

@app.task(bind=True, acks_late=True, max_retries=3, retry_backoff=True)
def charge_order(self, order_id: str, idem_key: str):
    with db_session() as s:
        try:
            s.add(ProcessedKey(key=idem_key))  # UNIQUE column
            s.flush()                            # raises on duplicate
        except IntegrityError:
            s.rollback()
            return {"status": "already_processed", "order_id": order_id}
        amount = payment_gateway.charge(order_id, idempotency_key=idem_key)
        s.commit()
        return {"status": "charged", "amount": amount}

Giftige berichten en de wachtrij voor onbestelbare berichten

Sommige berichten kunnen nooit slagen: payloads met een onjuist formaat, verwijzingen naar verwijderde rijen of een bug die altijd een fout veroorzaakt. Ze eindeloos opnieuw proberen verspilt workers en overspoelt je logboeken. Dit zijn giftige berichten.

Een dead-letter-wachtrij (DLQ) is een aparte wachtrij waarin uitgeputte of afgewezen berichten worden geparkeerd voor inspectie, waarschuwingen of handmatig opnieuw afspelen. Met RabbitMQ declareer je een wachtrij met een argument x-dead-letter-exchange; berichten die worden afgewezen (nack met requeue=False) of die een TTL overschrijden, worden door de broker automatisch daarheen gerouteerd.

from kombu import Exchange, Queue

dead_exchange = Exchange("dlx", type="direct")

task_queues = (
    Queue(
        "payments",
        Exchange("payments"),
        routing_key="payments",
        queue_arguments={
            "x-dead-letter-exchange": "dlx",
            "x-dead-letter-routing-key": "payments.dead",
        },
    ),
    Queue("payments_dead", dead_exchange, routing_key="payments.dead"),
)

Uitgeputte taken naar de DLQ routeren

De broker zet een bericht bij afwijzing in de dead-letter-wachtrij, maar het mechanisme voor opnieuw proberen van Celery wijst niet automatisch af wanneer max_retries is bereikt — het markeert de taak alleen als FAILED. Als je uitgeputte taken naar een DLQ wilt sturen, vang je MaxRetriesExceededError op (of detecteer je de laatste poging) en stuur je de payload expliciet door naar je dead-lettertaak of -wachtrij.

De dead-letterhandler mag nooit opnieuw verwerken — deze legt de fout vast, verstuurt een waarschuwing en slaat de payload op, zodat een beheerder deze na het oplossen van de hoofdoorzaak opnieuw kan afspelen.

from celery import Celery
from celery.exceptions import MaxRetriesExceededError

app = Celery("jobs", broker="redis://localhost:6379/0")

@app.task(bind=True, max_retries=5, retry_backoff=True)
def process_event(self, payload: dict):
    try:
        do_work(payload)
    except TransientError as exc:
        try:
            raise self.retry(exc=exc)
        except MaxRetriesExceededError:
            dead_letter.delay(payload, reason=str(exc))  # park it
    except PermanentError as exc:
        dead_letter.delay(payload, reason=str(exc))      # never retry

@app.task
def dead_letter(payload: dict, reason: str):
    store_failed_message(payload, reason)
    alert_oncall(reason)

Alles samenbrengen

Een veerkrachtige taak voor productie combineert alle onderdelen:

  • acks_late, zodat crashes leiden tot nieuwe afleveringen in plaats van verloren werk.
  • autoretry_for alleen voor tijdelijke fouten, met exponentiële wachttijd + jitter.
  • Een idempotentiesleutel die door een unieke beperking wordt bewaakt, zodat een nieuwe aflevering geen kwaad kan.
  • Een dead-letter-route voor permanente fouten en uitgeputte pogingen.

Het mentale model: probeer het tijdelijke opnieuw, verwijder duplicaten van het dubbele en stuur het uitzichtloze naar de dead-letter-wachtrij. Elk mechanisme dekt een ander foutscenario af — samen zorgen ze ervoor dat een worker veilig kan falen zonder gegevens stilletjes te beschadigen.

@app.task(
    bind=True, acks_late=True,
    autoretry_for=(TransientError,),
    max_retries=5, retry_backoff=True, retry_jitter=True,
)
def handle_webhook(self, payload: dict, idem_key: str):
    if already_processed(idem_key):       # unique-constraint check
        return "duplicate-ignored"
    try:
        result = apply_effect(payload, idem_key)
    except PermanentError as exc:
        dead_letter.delay(payload, reason=str(exc))
        return "dead-lettered"
    mark_processed(idem_key)
    return result

Korte controle

Je Celery-taak belast een creditcard en draait met acks_late=True. Omdat de broker berichten ten minste één keer aflevert, wordt hetzelfde bericht soms twee keer verwerkt. Wat is de juiste primaire verdediging tegen dubbele afschrijvingen?

Samenvatting

Je hebt geleerd hoe je Celery-taken bestand maakt tegen fouten uit de echte wereld:

  • Celery levert berichten ten minste één keer af; acks_late=True beschermt tegen crashes van workers, maar garandeert dat taken soms dubbel worden uitgevoerd.
  • Exponentiële wachttijd met jitter (via retry_backoff / autoretry_for of handmatig via self.retry) handelt tijdelijke fouten af zonder achterliggende services te overbelasten — probeer alleen tijdelijke fouten opnieuw.
  • Idempotentiesleutels met een unieke beperking maken dubbele afleveringen onschadelijk en houden de controle atomair met het neveneffect.
  • Dead-letter-wachtrijen parkeren giftige berichten en uitgeputte pogingen voor waarschuwingen en handmatig opnieuw afspelen, in plaats van eindeloos te blijven herhalen.

Onthoud de regel: probeer het tijdelijke opnieuw, verwijder duplicaten van het dubbele en stuur het uitzichtloze naar de dead-letter-wachtrij.

Gratis beginnen

Leer Bootcamp backendontwikkeling met FastAPI met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
21
Lessen
84

Veelgestelde vragen

Is de les “Retries, idempotentie en dead-letterafhandeling” gratis?

Ja — je kunt hier op het web alle 3 lessen van het leerpad Bootcamp backendontwikkeling met FastAPI, waaronder “Retries, idempotentie en dead-letterafhandeling”, gratis volledig lezen. Daarna ontgrendelt CoddyKit PRO alle lessen, plus interactieve oefeningen met een ingebouwde code-editor en een AI-tutor die 24/7 beschikbaar is. De cursus Bootcamp backendontwikkeling met FastAPI bevat in totaal 4 lessen.

Wat leer ik in “Retries, idempotentie en dead-letterafhandeling”?

Maak taken robuust met exponentiële backoff, idempotentiesleutels en dead-letter-routing voor onbruikbare berichten. Je oefent met Bootcamp backendontwikkeling met FastAPI door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Bootcamp backendontwikkeling met FastAPI te beginnen?

Ervaring vooraf is niet nodig. Bootcamp backendontwikkeling met FastAPI op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 3 van 4.

Hoe lang duurt de les “Retries, idempotentie en dead-letterafhandeling”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Bootcamp backendontwikkeling met FastAPI?

Ja. Elke les over Bootcamp backendontwikkeling met FastAPI bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Lichtgewicht offloading met BackgroundTasks
  2. Celery-workers koppelen aan een FastAPI-app
  3. Retries, idempotentie en dead-letterafhandeling
  4. Geplande en periodieke jobs met Celery Beat
← Terug naar Bootcamp backendontwikkeling met FastAPI