Background Tasks

RoadmapsPython Backend

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

Common mistakes

Recall questions

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)

Next: Uvicorn, Gunicorn, and Workers

Return to Python Backend Roadmap