FastAPI Essentials

Course Content

FastAPI Essentials

1 sections · 32 lessons

What is the purpose of the BackgroundTasks class in FastAPI?


The trace write leaves the user's wait0.00 srequest arrives0.02 sresponse sent2.00 strace savedclient isdone hereSame process: a restart between the last two steps loses the task.
Background tasks remove work from the user's latency, not from your server, and they vanish if the process dies.

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.

Python
import asynciofrom fastapi import BackgroundTasks, FastAPIfrom pydantic import BaseModelapp = FastAPI()class ChatRequest(BaseModel):    session_id: str    prompt: strasync def log_trace(session_id: str, prompt: str, answer: str) -> None:    await asyncio.sleep(2)                     # pretend: write to a tracing store    print(f"trace saved for {session_id}")@app.post("/chat")async def chat(req: ChatRequest, tasks: BackgroundTasks):    answer = "Your refund is on its way."      # pretend: await llm.generate(...)    tasks.add_task(log_trace, req.session_id, req.prompt, answer)    return {"answer": answer}

Run under Uvicorn and called with a real HTTP client:

Text
client got 200 {'answer': 'Your refund is on its way.'} after 0.02s[server, 2 s later] trace saved for s-42

The 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 yield dependency'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_task starts immediately and nothing tracks it; if you drop the reference it can even be garbage-collected. BackgroundTasks is tied to the response and runs after it.