Background Tasks
Scenario
A user submits a signup form. Your API hits the database, then calls a third-party email service to send a welcome email. The email API takes 2.5 seconds to respond. The user's browser spins for 3 seconds before showing 'Welcome!', and they click 'Submit' twice in frustration.
How do you return the HTTP response immediately while the server finishes the work?
Mental model
It's like paying at a drive-thru window. The cashier takes your money, gives you a receipt, and tells you to pull forward. The transaction is finished, but your food is still being made in the background.
A background task is a function executed after the HTTP response has been sent to the client. In FastAPI, you declare it by injecting BackgroundTasks into your endpoint. FastAPI accepts the task, instantly returns the response, and then runs the task on the same event loop (or threadpool) behind the scenes. This decouples slow, non-critical work from the user's waiting time.
Deep dive
If your endpoint does work that the client doesn't need to wait for—like sending a notification, syncing data to a CRM, or compressing an image—doing it before returning the response slows down your API. Every second the client waits is a second a worker is tied up.
Synchronous processing blocks the client and ties up the worker process.
FastAPI provides a built-in `BackgroundTasks` class. You inject it into your endpoint, then call `.add_task(func, *args, **kwargs)`. The framework remembers the task, returns the JSON response to the user immediately, and then invokes your function.
BackgroundTasks defers execution until after the HTTP response is sent.
Because the task runs in the same memory space and same worker process, it's lightweight and requires zero extra infrastructure. No Redis, no Celery, no separate worker containers. If it's an `async def` function, it runs on the event loop; if it's a regular `def`, FastAPI runs it in a threadpool so it doesn't block the loop.
Built-in tasks need zero infrastructure and handle both async and sync safely.
The catch: background tasks are ephemeral. If the uvicorn worker crashes, restarts, or runs out of memory before the task finishes, the task is gone forever. There is no retry mechanism and no persistence. For 'must-happen' tasks like processing payments, you need a real message queue.
FastAPI tasks are not durable. Use a durable message queue like Celery for critical work.
Code examples
The slow, blocking way (User waits)
from fastapi import APIRouter
import time
router = APIRouter()
def send_email(email: str, message: str):
time.sleep(3) # simulates slow third-party API
print(f"Sent to {email}: {message}")
@router.post("/signup")
def signup(email: str):
# The client waits 3 seconds for this to finish!
send_email(email, "Welcome to our app!")
return {"message": "User created"}
The endpoint doesn't return until `send_email` completes. The user stares at a loading spinner, degrading UX and wasting server connection capacity.
The FastAPI BackgroundTasks way (Instant response)
from fastapi import APIRouter, BackgroundTasks
import time
router = APIRouter()
def send_email(email: str, message: str):
time.sleep(3) # Safe: runs in a threadpool because it's a `def`
print(f"Sent to {email}: {message}")
@router.post("/signup")
def signup(email: str, background_tasks: BackgroundTasks):
# Queue the task, don't wait for it
background_tasks.add_task(send_email, email, "Welcome to our app!")
# Returns instantly!
return {"message": "User created, email sending in background"}
Injecting `BackgroundTasks` lets us defer the work. FastAPI returns the JSON immediately, then passes the task to a threadpool (since `send_email` is a sync function) so it doesn't block the main event loop.
When NOT to use FastAPI BackgroundTasks
@router.post("/process-payment")
async def process_payment(order_id: str, background_tasks: BackgroundTasks):
# DANGEROUS! If the pod restarts, this task is lost forever.
# The user was charged, but the order is never fulfilled.
background_tasks.add_task(fulfill_order, order_id)
return {"status": "processing"}
# Instead, for critical tasks, push to a durable queue:
# await redis.lpush("fulfillment_queue", order_id)
Built-in background tasks have no persistence or retry logic. If the server crashes or the deploy restarts the worker while a task is running, it's silently dropped.
Key points
- Runs after the response: The HTTP response is sent to the client before the background task begins executing.
- Zero infrastructure: Runs in the same worker process. You don't need Redis or a separate worker deployment.
- Sync vs Async safe: FastAPI handles `def` functions in a threadpool and `async def` on the event loop automatically.
- Not durable: If the server process dies, queued and running tasks are lost completely. No built-in retries.
Common mistakes
- Using BackgroundTasks for mission-critical operations: Putting payment processing or critical data syncing in a FastAPI background task. If the uvicorn worker crashes, the task is lost. You must use a durable queue (Celery, ARQ, SQS) for tasks that cannot be dropped.
- Passing database sessions that close early: If you pass a SQLAlchemy session from a FastAPI dependency into a background task, the dependency will close the session when the HTTP response returns. The background task then tries to use a closed session and crashes.
Recall questions
- When does a FastAPI BackgroundTask actually start executing?
- If you add a standard `def` (sync) function to a background task, does it freeze the async event loop?
- Why shouldn't you use FastAPI's built-in background tasks for processing payments?
Questions & answers
You need to send an analytics event to a third-party service every time a user logs in. How would you implement this in FastAPI to ensure login remains fast?
I would use FastAPI's `BackgroundTasks` injected into the login endpoint. I'd add the analytics tracking function to it so it executes after the login response is returned. Since analytics aren't mission-critical, losing an event on a rare server crash is an acceptable tradeoff for not needing a full message queue infrastructure.
Approach: Match the tool to the constraint. BackgroundTasks are perfect for 'fire-and-forget' tasks where low overhead is more important than absolute durability.
Your FastAPI background task needs to write to the database using SQLAlchemy, but it keeps throwing a 'Session closed' error. What is causing this?
The database session was likely injected into the endpoint via `Depends()` and passed into the background task. Dependencies are torn down (closing the session) immediately after the HTTP response is sent. By the time the background task runs, the session is already closed. The fix is to create a new, separate session inside the background task itself.
Approach: Identify the lifecycle mismatch. Endpoints live until the response; background tasks live *after* the response. Therefore, they cannot share resources that are tied to the endpoint's lifecycle.
Continue learning
Previous: async vs def Endpoints
Previous: Dependency Injection (Depends)