Often a request should respond immediately and do slow work afterwards: sending emails, writing audit logs, pushing notifications. BackgroundTasks runs a function after the response has been sent, inside the same process.
from fastapi import FastAPI, BackgroundTasks
app = FastAPI()
def send_email(email_to: str, message: str):
# pretend this takes 2 seconds
with open("emails.log", "a") as f:
f.write(f"{email_to}: {message}\n")
@app.post("/users/")
async def create_user(email: str, background_tasks: BackgroundTasks):
background_tasks.add_task(send_email, email, message="Welcome!")
return {"message": "User created - email will be sent shortly"}
The client gets the 200 response right away; send_email runs afterwards. add_task(fn, *args, **kwargs) accepts the callable plus its arguments.
async def task: awaited directly on the event loop — use for async I/O (httpx, async DB).def task: executed in the thread pool — safe for blocking code.add_task calls run sequentially in the order added.def write_audit_log(background_tasks: BackgroundTasks, user: str = Header()):
def log(action: str):
print(f"{user} did {action}")
background_tasks.add_task(log, "created_item")
# nothing to return - dependency just schedules side effects
@app.post("/items/", dependencies=[Depends(write_audit_log)])
def create_item(item: Item):
return item
You can inject BackgroundTasks in dependencies and endpoints alike — tasks from all of them accumulate and run after the response.
async def process_report(report_id: int, db_session_factory):
async with db_session_factory() as db:
report = await db.get(Report, report_id)
report.status = "processing"
await db.commit()
# ... heavy work ...
report.status = "done"
await db.commit()
@app.post("/reports/", status_code=202)
async def request_report(background_tasks: BackgroundTasks):
report_id = create_pending_report()
background_tasks.add_task(process_report, report_id, async_session_factory)
return {"report_id": report_id, "status": "queued"}
Return 202 Accepted to signal "received, processing later" — the semantically correct status for background work, with a status field the client can poll.
A: No. Starlette sends the response first, then awaits the task functions. The client is already gone when the task runs — that is why tasks cannot change the response.
A: Not the one from get_db — by the time the task runs, the request lifecycle (and the session) has ended. Create a fresh session inside the task (pass a session factory), or load all needed data before returning the response.
A: BackgroundTasks for fire-and-forget work that may be lost occasionally without harm (logs, non-critical notifications, cache warming) and that finishes in seconds. Celery/RQ for jobs that must not be lost, need retries/timeouts, take minutes, or must run on separate worker machines.
BackgroundTasks is the zero-infrastructure way to answer fast and work later: inject it, add_task(fn, ...), return 202. It has no delivery guarantees — the moment a task matters for the business, move it to a real message queue.