Blogs / FastAPI BackgroundTasks vs. Celery: Wann was verwenden

FastAPI BackgroundTasks vs. Celery: Wann was verwenden

Veröffentlicht
27. August 2026
Autor
Faizan Nadeem
Tags
FastAPI Python Celery Backend Development
Metallkomponenten bewegen sich auf einem industriellen Förderband in einer Fabrikmontagelinie
Foto von Salvador Escalante auf Unsplash

BackgroundTasks sieht so aus, als würde es dasselbe Problem lösen wie Celery — Arbeit ausführen, nachdem die Response rausgegangen ist, ohne dass der Nutzer darauf warten muss. Es ist in FastAPI eingebaut, braucht keine zusätzliche Infrastruktur, und der Code dafür ist drei Zeilen lang. Genau diese Einfachheit ist der Grund, warum Teams dazu greifen, um Bestätigungs-E-Mails zu senden, die Erstellung eines Reports anzustoßen oder einen Audit-Log-Eintrag zu schreiben. Und es funktioniert — bis mitten in einem Request ein Deploy läuft, oder der Prozess per OOM-Kill beendet wird, oder der Pod-Autoscaler entscheidet, eine Instanz zu terminieren — und die Aufgabe, die „im Hintergrund“ lief, schlicht nie stattfindet, ohne Fehler, ohne Retry und ohne irgendeinen Hinweis darauf, dass sie jemals hätte ausgeführt werden sollen.

Dieser Ausfallmodus ist kein BackgroundTasks-Bug. Es ist BackgroundTasks, das genau das tut, wofür es entworfen wurde — eine Coroutine im selben Prozess nach der Response ausführen, ohne Persistenzschicht dahinter. Der eigentliche Fehler ist, es für Arbeit zu verwenden, die Garantien benötigt, die nur eine echte Task-Queue bietet. Dieser Beitrag erklärt, was BackgroundTasks tatsächlich ist, unter welchen konkreten Bedingungen es Arbeit stillschweigend verliert, was Celery hinzufügt, um das zu beheben, und ein konkretes Framework, um zwischen beiden zu wählen, statt einfach standardmäßig das zu nehmen, was man zuerst eingerichtet hat.

Sie lernen:

  • Was BackgroundTasks unter der Haube tatsächlich macht und warum es im selben Prozess wie der Request läuft
  • Die konkreten Fehlerszenarien — Deploys, Crashes, Autoscaling — in denen BackgroundTasks stillschweigend Arbeit verliert
  • Was Celery strukturell hinzufügt: einen Broker, Persistenz, Retries und unabhängige Worker-Prozesse
  • Ein Entscheidungsframework basierend auf Durability-Anforderungen, Retry-Bedarf und erwarteter Task-Dauer
  • Wie man beide kombiniert — BackgroundTasks für wirkliches Fire-and-forget, Celery für alles, was eine Garantie braucht
  • Funktionsfähigen Code für beides, einschließlich einer Celery-Retry-Policy und eines FastAPI-Endpoints, der dorthin dispatcht

Inhaltsverzeichnis

  1. Die Grundlagen
  2. Wie BackgroundTasks tatsächlich funktioniert
  3. Wo BackgroundTasks stillschweigend Arbeit verliert
  4. Was Celery hinzufügt
  5. Das Entscheidungsframework
  6. Hybride Muster
  7. Codebeispiele
  8. Leichtgewichtigere Alternativen zu Celery
  9. Häufige Fallstricke
  10. Best Practices für Produktion

Die Grundlagen

Zwei unterschiedliche Werkzeuge für zwei unterschiedliche Probleme

BackgroundTasks ist ein Starlette-Feature, das FastAPI direkt bereitstellt: eine Möglichkeit, eine Funktion so zu planen, dass sie nach dem Senden der HTTP-Response innerhalb desselben Worker-Prozesses ausgeführt wird, der den Request bearbeitet hat. Celery ist eine verteilte Task-Queue: ein separates System mit eigenen Worker-Prozessen, einem Message-Broker (typischerweise Redis oder RabbitMQ) zwischen Ihrer App und diesen Workern sowie einem Results-/State-Backend, das einen Neustart des App-Prozesses überlebt.

Die Ähnlichkeit auf oberflächlicher Ebene — „später ausführen, die Response nicht blockieren“ — verdeckt einen viel größeren strukturellen Unterschied: BackgroundTasks hat überhaupt keine Persistenzschicht. Wenn der Prozess, der die Task geplant hat, stirbt, bevor die Task fertig ist, ist die Task weg. Der Broker von Celery persistiert die Task in dem Moment, in dem sie enqueued wird, unabhängig davon, ob Ihr API-Prozess überhaupt noch läuft, wenn ein Worker sie abholt.

Warum diese Unterscheidung oft übersehen wird

Die meisten Tutorials führen BackgroundTasks mit echten Fire-and-forget-Beispielen ein — Logging, Cache-Warming — bei denen der gelegentliche Verlust einer Task harmlos ist. Teams verwenden dann dasselbe Muster erneut für Dinge, deren Verlust nicht harmlos ist: das Senden einer Passwort-Reset-E-Mail, das Abbuchen einer Zahlung, das Erzeugen eines Dokuments, auf das ein Nutzer wartet. Die API sieht in beiden Fällen identisch aus. Die Konsequenz eines Fehlers nicht.

Wie BackgroundTasks tatsächlich funktioniert

Ein BackgroundTasks-Objekt wird in eine Route injiziert, Funktionen werden darauf registriert, und FastAPI führt sie aus, nachdem die Response gesendet wurde — aber weiterhin innerhalb desselben asynchronen Event-Loops und desselben Worker-Prozesses:

from fastapi import FastAPI, BackgroundTasks

app = FastAPI()

def send_confirmation_email(email: str):
    # runs after the response has already gone out
    email_client.send(email, "Thanks for signing up")

@app.post("/signup")
async def signup(email: str, background_tasks: BackgroundTasks):
    create_user(email)
    background_tasks.add_task(send_confirmation_email, email)
    return {"status": "created"}

Der Nutzer erhält seine Response in dem Moment, in dem create_user fertig ist — auf send_confirmation_email wartet er überhaupt nicht. Das ist das gesamte Wertversprechen, und es ist wirklich nützlich. Aber beachten Sie, was nicht passiert: Es gibt keine Queue, keinen Broker, keinen separaten Worker und nichts, was verfolgt, ob send_confirmation_email jemals tatsächlich abgeschlossen wurde. Es ist ein geplanter Coroutine-Aufruf innerhalb desselben Prozesslebenszyklus wie der Request, der ihn ausgelöst hat — näher an asyncio.create_task mit etwas angenehmerer Ergonomie als an irgendetwas, das einer Job-Queue ähnelt.

Wenn send_confirmation_email selbst synchron und blockierend ist, läuft es außerdem innerhalb derselben Threadpool- oder Event-Loop-Beschränkungen, die in Warum Ihr FastAPI-Endpoint den Event Loop blockiert beschrieben werden — eine langsame Hintergrund-Task kann den Durchsatz des restlichen Prozesses also weiterhin verschlechtern, wenn sie nicht so geschrieben ist, dass sie mit dem Loop kooperiert. Eine synchrone Funktion, die mit add_task registriert wird, läuft im Threadpool von Starlette genau wie eine def-Route, während eine async def-Funktion, die auf dieselbe Weise registriert wird, direkt auf dem Event Loop läuft — daher kann eine Hintergrund-Task, die blockiert, ohne zu awaiten, laufende Requests ausbremsen, nicht nur andere Hintergrundarbeit. Das übersieht man leicht, gerade weil die Task auf den ersten Blick vom Request-/Response-Zyklus entkoppelt aussieht.

Auch das Exception-Handling lässt sich leicht falsch einschätzen. Wenn eine registrierte Task eine Exception wirft, lässt FastAPI die Response nicht crashen — sie wurde bereits gesendet — aber standardmäßig ist die Exception nur sichtbar, wenn Ihr Logging unhandled Exceptions aus Hintergrund-Tasks gezielt erfasst; ein bloßes try/except um die Route selbst sieht sie nie, weil die Route-Funktion bereits zurückgegeben hat, wenn die Task läuft.

Wo BackgroundTasks stillschweigend Arbeit verliert

Jeder dieser Fälle ist ein normales, erwartbares Ereignis in einem Produktions-Deployment — kein Randfall:

Deploys. Ein Rolling Deploy sendet SIGTERM an den alten Prozess. Wenn eine Hintergrund-Task gerade läuft (oder noch nicht gestartet hat, weil sie hinter anderer Arbeit im selben Event Loop ansteht), wenn das Grace-Period-Fenster des Prozesses abläuft und er per SIGKILL beendet wird, ist diese Task dauerhaft weg, ohne dass irgendetwas darüber geloggt wurde.

Scale-down durch Autoscaling. Derselbe Mechanismus wie bei einem Deploy — ein Orchestrator (Kubernetes, ECS) entscheidet, eine Instanz bei geringer Last zu terminieren, und jede BackgroundTasks-Arbeit, die auf dieser Instanz läuft, verschwindet mit ihr.

Prozess-Crashes und OOM-Kills. Ein nicht zusammenhängendes Memory Leak oder ein unerwartet großer Request an anderer Stelle im selben Prozess kann dazu führen, dass der gesamte Worker per OOM-Kill beendet wird und alle ausstehenden Hintergrund-Tasks mitnimmt — sogar Tasks, die nichts mit der Ursache des Crashes zu tun hatten.

Kein Retry bei Fehlern. Wenn send_confirmation_email eine Exception wirft — etwa weil der E-Mail-Provider in ein Timeout läuft — retried BackgroundTasks sie nicht. Standardmäßig wird die Exception geschluckt (sichtbar nur, wenn Sie Exception-Logging dafür verdrahtet haben), und die Task wird schlicht nie abgeschlossen. Es gibt keine Dead-Letter-Queue, kein Backoff, keinen zweiten Versuch.

Keine prozessübergreifende Sichtbarkeit. Wenn Sie mehrere Uvicorn-/Gunicorn-Worker betreiben, hat ein BackgroundTasks-Job, der auf Worker 2 geplant wurde, keinerlei Beziehung zu Worker 1 — Sie können ihn nirgends inspizieren, retrien oder abbrechen außer in genau dem Prozess, der ihn geplant hat, und die Logs dieses Prozesses sind der einzige Nachweis dafür, dass er existierte.

Nichts davon macht BackgroundTasks kaputt. Es macht es zu einem Werkzeug für Arbeit, deren gelegentlichen Verlust Sie sich tatsächlich leisten können — nicht zu einer Allzweck-Job-Queue mit einfacherer API.

Was Celery hinzufügt

Die Architektur von Celery beantwortet direkt jede der obigen Lücken, indem sie eine dauerhafte, unabhängige Schicht zwischen „dem Request, der die Arbeit ausgelöst hat“ und „dem Prozess, der die Arbeit ausführt“ einzieht:

  • Einen Message-Broker (Redis/RabbitMQ), der die Task persistiert. In dem Moment, in dem .delay() aufgerufen wird, wird die Task serialisiert und in den Broker geschrieben — unabhängig vom Lebenszyklus des FastAPI-Prozesses. Wenn der API-Prozess eine Millisekunde später stirbt, ist die Task bereits sicher im Broker.
  • Unabhängige Worker-Prozesse. Celery-Worker sind separate Prozesse (oft sogar separate Container oder Hosts), die Tasks aus dem Broker ziehen. Ein Deploy Ihrer API betrifft sie nicht; ein Deploy Ihrer Worker betrifft Ihre API nicht.
  • Eingebaute Retries mit Backoff. Eine Task kann max_retries und eine Backoff-Policy deklarieren, sodass ein transienter Fehler — etwa eine kurzzeitig nicht verfügbare Downstream-API — automatisch erneut versucht wird, statt stillschweigend zu sterben.
  • Ein Results-Backend. Der Zustand einer Task (pending, success, failure und der Return-Value) ist nachträglich von jedem Prozess aus abfragbar, wodurch „wurde das tatsächlich abgeschlossen?“ zu einer beantwortbaren Frage wird statt zu einer Übung im Log-Durchsuchen.
  • Scheduling und Rate Limiting. Periodische Tasks (celery beat), Rate Limits pro Task-Typ und Prioritäts-Queues sind First-Class-Features und nicht etwas, das Sie auf BackgroundTasks von Hand aufbauen müssten.

Der Trade-off ist echte Infrastruktur: ein Broker, den man betreiben und überwachen muss, Worker-Prozesse, die unabhängig deployt und skaliert werden müssen, und ein tatsächlich komplexeres mentales Modell — Task-Serialisierung, Idempotenz und Broker-Connection-Handling sind jetzt auf eine Weise Ihr Problem, wie sie es bei BackgroundTasks nicht waren.

Das Entscheidungsframework

Drei Fragen entscheiden fast jeden Fall:

1. Können Sie es sich leisten, diese Task vollständig und stillschweigend zu verlieren, ohne Retry?
Wenn ja — ein Metrics-Ping, ein Best-Effort-Cache-Warm, ein Analytics-Event — dann ist BackgroundTasks in Ordnung, und Celery hinzuzufügen wäre reiner Overhead. Wenn nein — alles, was mit Geld zu tun hat, alles, auf dessen Ergebnis ein Nutzer ausdrücklich wartet, alles mit Compliance- oder Audit-Anforderung — dann brauchen Sie die Durability von Celery.

2. Muss die Task ein Deploy oder ein Scale-down-Ereignis überleben?
BackgroundTasks-Arbeit überlebt nur, wenn der Prozess, der sie geplant hat, lange genug am Leben bleibt, um sie zu beenden. Wenn Sie häufig deployen (mehrmals täglich, wie bei CI/CD üblich) und Tasks länger laufen können als Ihr Graceful-Shutdown-Fenster, dann verliert BackgroundTasks Arbeit in vorhersehbarer Regelmäßigkeit und nicht nur selten.

3. Braucht die Task eine Retry-Policy, Scheduling oder prozessübergreifende Sichtbarkeit?
Wenn ein Fehler automatisch retried werden muss, wenn die Task nach einem Zeitplan unabhängig von Requests laufen soll oder wenn Sie den Task-Status aus einem anderen Prozess heraus prüfen müssen (ein Admin-Dashboard, eine separate API), dann sind genau dafür Broker und Results-Backend von Celery da — BackgroundTasks hat für keinen dieser drei Punkte einen Mechanismus.

SignalBackgroundTasksCelery
Task-DauerSekunden, nicht MinutenSekunden bis Stunden
VerlusttoleranzVollständig tolerantBraucht Durability
Retry bei FehlernNichts eingebautEingebaut, mit Backoff
Überlebt ProzessneustartNeinJa
Prozessübergreifende SichtbarkeitNeinJa (Results-Backend)
Erforderliche InfrastrukturKeineBroker + Worker-Prozesse
Scheduling (cron-artig)NeinJa (celery beat)

Hybride Muster

Die meisten reifen FastAPI-Codebasen verwenden bewusst beides, statt ein Werkzeug für die gesamte App auszuwählen. Eine gängige und effektive Aufteilung: BackgroundTasks für Arbeit verwenden, die billig zu wiederholen oder wirklich entbehrlich ist, und für alles mit Durability- oder Retry-Anforderung an Celery dispatchen — oft sogar aus demselben Endpoint.

@app.post("/orders")
async def create_order(payload: OrderCreate, background_tasks: BackgroundTasks):
    order = await create_order_record(payload)

    # Disposable — fine to lose occasionally, no retry needed
    background_tasks.add_task(log_analytics_event, "order_created", order.id)

    # Durable — must survive a deploy, needs a retry policy
    send_order_confirmation.delay(order.id)

    return order

Die Faustregel: Wenn es Sie in einem Post-mortem verärgern würde festzustellen, dass eine Task stillschweigend nie gelaufen ist, dann gehört sie in Celery. Wenn Sie nur mit den Schultern zucken würden, ist BackgroundTasks genau das richtige Maß an Tooling.

Codebeispiele

Ein minimales Celery-Setup, das das obige Framework widerspiegelt — Retries mit exponentiellem Backoff für eine wirklich wichtige Task:

# celery_app.py
from celery import Celery

celery_app = Celery(
    "worker",
    broker="redis://localhost:6379/0",
    backend="redis://localhost:6379/1",
)

@celery_app.task(
    bind=True,
    max_retries=5,
    default_retry_delay=10,  # seconds, before backoff kicks in
)
def send_order_confirmation(self, order_id: int):
    try:
        order = fetch_order(order_id)
        email_client.send(order.customer_email, render_receipt(order))
    except EmailProviderTimeout as exc:
        # exponential-ish backoff: 10s, 20s, 40s...
        raise self.retry(exc=exc, countdown=10 * (2 ** self.request.retries))
# app/orders/router.py
from app.workers.celery_app import send_order_confirmation

@router.post("/")
async def create_order_endpoint(payload: OrderCreate):
    order = await create_order(payload)
    send_order_confirmation.delay(order.id)
    return order

.delay(order.id) kehrt sofort zurück — es stellt die Task nur im Broker ein — daher bleibt die Response-Zeit des Endpoints unbeeinflusst, während die eigentliche Abschlussgarantie jetzt im Broker und in der Retry-Policy von Celery lebt und nicht mehr im FastAPI-Prozess. Diese Art von Modulgrenze — ein workers/-Package, in das der Router dispatcht, ohne dessen Interna zu besitzen — passt natürlich zu dem Domain-driven-Layout aus FastAPI-Projektstruktur, die Wachstum überlebt.

Leichtgewichtigere Alternativen zu Celery

Celery ist nicht die einzige durable Option, und für eine Async-first-FastAPI-Codebasis ist es oft nicht die natürlichste Wahl — das Worker-Modell von Celery stammt aus der Zeit vor asyncio und behandelt Async-Tasks eher als nachträglich angeschraubtes Feature denn als First-Class-Design.

arq ist eine Redis-gestützte Queue, die speziell von Grund auf für asyncio gebaut wurde. Task-Funktionen sind async def, Worker betreiben ihren eigenen Event Loop, und es gibt keine Brücke von synchron nach async, über die man nachdenken müsste:

# worker.py
async def send_order_confirmation(ctx, order_id: int):
    order = await fetch_order(order_id)
    await email_client.send_async(order.customer_email, render_receipt(order))

class WorkerSettings:
    functions = [send_order_confirmation]
    redis_settings = RedisSettings(host="localhost")
# dispatching from FastAPI
redis = await create_pool(RedisSettings(host="localhost"))
await redis.enqueue_job("send_order_confirmation", order.id)

Dramatiq und RQ liegen in einem ähnlichen Bereich — einfacheres Setup als Celery, Redis als Broker und eine kleinere Feature-Oberfläche (zum Beispiel kein celery beat-äquivalentes Scheduling im Core von RQ) im Austausch für weniger operativen Overhead.

Das obige Entscheidungsframework gilt weiterhin, egal welche durable Queue Sie wählen — die Frage war nie konkret „Celery oder nichts“, sondern „braucht diese Task überhaupt Broker-gestützte Durability“. Celery bleibt die richtige Standardwahl, wenn Sie sein Ökosystem brauchen — reifes Scheduling sowie Routing- und Monitoring-Tooling, zu dem die neueren Alternativen noch aufschließen — aber für einen Greenfield-Async-FastAPI-Service lohnt es sich, arq zuerst zu evaluieren, gerade weil es vermeidet, einen Sync-first-Task-Runner in eine ansonsten Async-first-Codebasis zu mischen.

Häufige Fallstricke

Fehler: BackgroundTasks für alles zu verwenden, was mit Zahlung oder Compliance zu tun hat. Eine stillschweigend verlorene Zahlungsbestätigung oder ein Audit-Log-Eintrag ist ein Geschäftsproblem, keine technische Unannehmlichkeit. Lösung: Alles mit rechtlicher, finanzieller oder Audit-Anforderung läuft über Celery, ohne Ausnahmen.

Fehler: anzunehmen, dass Celery-Tasks automatisch idempotent sind. Eine retried Task führt die gesamte Funktion erneut aus — wenn send_order_confirmation nicht sicher zweimal laufen kann, kann ein Retry nach einem teilweisen Fehler doppelt senden. Lösung: Tasks idempotent entwerfen (check-before-act oder Idempotency Keys verwenden), wann immer Retries aktiviert sind.

Fehler: wirklich lang laufende Arbeit in BackgroundTasks zu legen. Eine Task, die minutenlang im selben Prozess wie Ihre API-Worker läuft, konkurriert um dieselben Ressourcen wie die Request-Verarbeitung. Lösung: Alles über ein paar Sekunden hinaus gehört in einen separaten Worker-Pool — genau das stellt Celery bereit.

Fehler: Celery für eine einzelne, unkritische Task aufzubauen. Die Betriebskosten eines Brokers und einer Worker-Flotte rechtfertigen keinen einzelnen Best-Effort-Analytics-Call. Lösung: Standardmäßig BackgroundTasks verwenden, bis eine konkrete Durability- oder Retry-Anforderung auftaucht.

Fehler: den Gesundheitszustand von Celery-Workern nicht zu überwachen. Eine abgestürzte oder überlastete Worker-Flotte akkumuliert stillschweigend einen wachsenden Backlog ohne sichtbares Symptom für Nutzer, bis es gravierend wird. Lösung: Queue-Tiefe und Worker-Liveness (Flower oder die Celery-Integration Ihres APM) als First-Class-Produktionsmetriken überwachen.

Best Practices für Produktion

  • Standardmäßig BackgroundTasks verwenden und bewusst upgraden. Keine Celery-Infrastruktur aufbauen, bevor eine konkrete Task tatsächlich die Garantien benötigt, die Celery bietet.
  • Celery-Tasks standardmäßig idempotent machen. Gehen Sie davon aus, dass jede Task mindestens einmal retried wird, denn irgendwann wird sie das.
  • Explizite Retry-Limits und Backoff setzen, niemals unbegrenzte Retries. Eine Task, die für immer gegen eine dauerhaft kaputte Abhängigkeit retried, wird nur zu einer anderen Art von Incident.
  • Task-Dispatch und Abschluss getrennt loggen. Zu wissen, dass eine Task geplant wurde, ist nicht dasselbe wie zu wissen, dass sie erfolgreich war — instrumentieren Sie beides.
  • Queue-Tiefe als alarmierbare Metrik behandeln. Ein wachsender, unverarbeiteter Celery-Backlog ist eines der frühesten Signale für einen Ausfall in einem Downstream-System.

Fazit

BackgroundTasks und Celery sind keine konkurrierenden Lösungen für dasselbe Problem — sie sind das richtige Werkzeug für zwei wirklich unterschiedliche Durability-Anforderungen, und der Fehler besteht darin, nach Einrichtungsaufwand zu entscheiden statt danach, was passiert, wenn mitten in einer Task ein Deploy landet. Wenn der stille Verlust der Arbeit ein echtes Problem wäre, dann ist das Ihre Antwort, egal wie viel einfacher BackgroundTasks heute aussieht. Die meisten FastAPI-Apps in Produktion verwenden am Ende bewusst beides, sobald sie genau einmal zu oft von einer verlorenen Task gebrannt wurden.

Wissen Sie tatsächlich, was mit Ihren laufenden Hintergrund-Tasks passiert, wenn Sie das nächste Mal deployen?

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