Course Content
FastAPI Essentials
1 sections · 32 lessons
What is the purpose of the BackgroundTasks class in FastAPI?
What you need to know
A request often has two parts: the work the user is waiting for, and follow-up work they do not care about. In a chat endpoint, the user waits for the answer, but not for the trace log you write for debugging. BackgroundTasks moves the second part out of the user's wait.
1import asyncio2from fastapi import BackgroundTasks, FastAPI3from pydantic import BaseModel45app = FastAPI()67class ChatRequest(BaseModel):8 session_id: str9 prompt: str1011async def log_trace(session_id: str, prompt: str, answer: str) -> None:12 await asyncio.sleep(2) # pretend: write to a tracing store13 print(f"trace saved for {session_id}")1415@app.post("/chat")16async def chat(req: ChatRequest, tasks: BackgroundTasks):17 answer = "Your refund is on its way." # pretend: await llm.generate(...)18 tasks.add_task(log_trace, req.session_id, req.prompt, answer)19 return {"answer": answer}Run under Uvicorn and called with a real HTTP client:
client got 200 {'answer': 'Your refund is on its way.'} after 0.02s[server, 2 s later] trace saved for s-42The 2-second trace write did not add anything to the user's wait. You declare tasks: BackgroundTasks as a parameter (FastAPI injects it, just like Request), then call add_task(function, *args). Tasks run in the order added, after the response is sent. An async def task runs on the event loop; a plain def task runs in the threadpool. Dependencies can also accept BackgroundTasks and add tasks; FastAPI merges them.
Know the limits
BackgroundTasks
- Same process as the web server
- Lost on crash, deploy or scale-down
- No retries, no status, no dashboard
- Good for work under a few seconds
A task queue (Celery, RQ, Arq, SQS)
- Separate worker processes
- Job is stored until done
- Retries, dead-letter queue, monitoring
- Good for minutes-long or must-not-lose work
Background tasks also share the worker's CPU and event loop. A heavy CPU task in the background slows down the live requests on that worker.
A real-life example
A food-delivery app's support bot writes three things after each answer: a trace row for debugging, an analytics event, and a "was this helpful?" push notification. Doing them inline added about 400 ms to every reply. Moving them into BackgroundTasks brought the reply time back to the LLM's own latency.
Then a product manager asked to generate embeddings for every uploaded restaurant menu "in the background" too. Each menu took 40 to 90 seconds, and during a deploy the pods restarted and about 200 menus were silently never indexed. That work moved to a Celery queue with retries and a status column in the database, while the small trace and analytics writes stayed as background tasks. The dividing line: if losing the task would cause a support ticket, it needs a queue.
Follow-up questions to expect
- "When exactly does the task run?" — After the response has been fully sent to the client, inside the same request cycle on the server.
- "Can the task use the request's database session?" — In current FastAPI, yes: with the default scope, a
yielddependency's cleanup runs after the background tasks finish. This behaviour has changed between versions, so many teams open a fresh session inside the task to keep it independent. - "How is this different from
asyncio.create_task?" —create_taskstarts immediately and nothing tracks it; if you drop the reference it can even be garbage-collected.BackgroundTasksis tied to the response and runs after it.