How-to: BackgroundTasks
How to run work after the response using FastAPI's BackgroundTasks.
1. Basic pattern
python
from fastapi import BackgroundTasks, FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
def send_notification(message: str) -> None:
# slow work (sending email, calling an external API, etc.)
print(f"Sending: {message}")
@app.post("/orders", status_code=201)
def create_order(
body: CreateOrderBody,
background_tasks: BackgroundTasks,
) -> JSONResponse:
order = process_order(body)
background_tasks.add_task(send_notification, f"Order {order.order_id} created")
return JSONResponse({"order_id": order.order_id}, status_code=201)2. Keeping it separate from the UseCase
To keep the UseCase HTTP-independent, don't pass BackgroundTasks into it. Receive the event in the handler layer and add it to BackgroundTasks there.
python
# ✅ the UseCase doesn't know about BackgroundTasks
class CreateOrderUseCase:
def execute(self, body: CreateOrderInput) -> CreateOrderOutput:
order = Order(...)
return CreateOrderOutput(order_id=order.order_id, notify_email=body.email)
# use BackgroundTasks in the handler layer
@app.post("/orders", status_code=201)
def create_order(
body: CreateOrderBody,
background_tasks: BackgroundTasks,
use_case: CreateOrderUseCase = Depends(get_use_case),
) -> JSONResponse:
result = use_case.execute(CreateOrderInput(email=body.email))
background_tasks.add_task(send_notification, result.notify_email)
return JSONResponse({"order_id": result.order_id}, status_code=201)3. Behavior under TestClient
Under TestClient, BackgroundTasks runs synchronously before the response is returned.
python
executed: list[str] = []
def track_task(msg: str) -> None:
executed.append(msg)
# in tests, BackgroundTasks runs synchronously
r = client.post("/orders", json={"email": "alice@example.com"})
assert r.status_code == 201
assert len(executed) == 1 # already executedIn production it runs asynchronously (after the response); note it runs synchronously in tests.
4. A failure does not cause a 500
If an exception is raised inside a BackgroundTasks task, the response has already been sent, so it does not become a 500. The error is logged.
python
def risky_task() -> None:
raise RuntimeError("Background task failed")
# the response returns 201 (the background error is hidden)
background_tasks.add_task(risky_task)For important work, don't rely on BackgroundTasks — use a job queue (Celery, ARQ, etc.).
5. Combining with async def
background_tasks.add_task() accepts both sync and async functions.
python
async def async_notification(email: str) -> None:
await send_email_async(email)
background_tasks.add_task(async_notification, user.email)