Python FastAPI Background Task Not Running After Returning Response
A FastAPI background task that seems to never run, despite being correctly added to the request, usually comes down to either an incorrect function signature preventing it from actually being scheduled, or the task genuinely running but failing silently in a way that produces no visible error in your normal application logs.
The Problem
You add a background task expecting it to execute after the response is sent, but whatever side effect it's supposed to produce (sending an email, writing a log entry, updating a record) never seems to happen:
@app.post("/orders")
async def create_order(order: OrderInput, background_tasks: BackgroundTasks):
background_tasks.add_task(send_confirmation_email, order.email)
return {"status": "created"}
The endpoint itself responds successfully, and no error appears anywhere in the visible application output, yet the confirmation email never actually arrives.
Why It Happens
FastAPI's BackgroundTasks genuinely does execute the scheduled function after the response is sent — when it appears not to run, the actual cause is usually one of these:
- An exception is raised inside the background task function, but since it runs after the response has already been sent, that exception doesn't propagate back to anywhere visible in the normal request/response cycle — it's typically only logged internally by the server (Uvicorn), and easy to miss if you're not specifically watching for it
- The background task function is defined as a regular synchronous function performing genuinely blocking I/O (like a slow, blocking HTTP request to an email provider), which can behave unexpectedly depending on how it's structured relative to FastAPI's async event loop, particularly if it blocks the event loop in a way that delays or effectively hides its own execution
- The task was added to a
BackgroundTasksinstance obtained incorrectly — for example, creating a new, separateBackgroundTasks()instance manually inside the function rather than receiving it through FastAPI's dependency injection, which means the tasks added to it are never actually connected to the response FastAPI sends - The server process itself is shut down or restarted (common during development with an auto-reloading server) before the background task, scheduled to run after the response, actually gets a chance to execute
The Fix
First, confirm you're using the BackgroundTasks instance provided through FastAPI's dependency injection, not a manually instantiated one — this is a subtle but critical distinction:
# Correct - FastAPI injects and tracks this instance
@app.post("/orders")
async def create_order(order: OrderInput, background_tasks: BackgroundTasks):
background_tasks.add_task(send_confirmation_email, order.email)
return {"status": "created"}
# Incorrect - a manually created instance is never connected to the actual response
@app.post("/orders")
async def create_order(order: OrderInput):
background_tasks = BackgroundTasks() # not the one FastAPI actually uses
background_tasks.add_task(send_confirmation_email, order.email)
return {"status": "created"}
Add explicit logging inside the background task function itself, at both the start and end (and within any try/except block), to confirm whether it's genuinely being invoked at all, and whether it completes successfully or fails partway through:
def send_confirmation_email(email: str):
logger.info(f"Background task started for {email}")
try:
# actual email sending logic
logger.info(f"Background task completed for {email}")
except Exception as e:
logger.error(f"Background task failed for {email}: {e}")
This explicit logging, especially the exception handling, surfaces failures that would otherwise be silently swallowed by FastAPI's background task execution, which doesn't propagate exceptions back to anywhere you'd normally be watching for them.
If the background task performs genuinely blocking I/O and is defined as a synchronous function, this is actually fine and expected in FastAPI's design — synchronous background tasks are run in a separate thread pool automatically, specifically so they don't block the main async event loop. But if you defined it as an async def function that itself contains blocking (non-async) calls, that combination can cause real event loop blocking issues:
# Problematic - async function with blocking call inside
async def send_confirmation_email(email: str):
requests.post(email_api_url, json={"to": email}) # blocking call inside an async function
# Better - use an async-native HTTP client if the function itself is async
async def send_confirmation_email(email: str):
async with httpx.AsyncClient() as client:
await client.post(email_api_url, json={"to": email})
For local development specifically, confirm the server isn't restarting or shutting down between the response being sent and the background task actually executing — if you're using an auto-reloading development server and testing with rapid, repeated requests, occasional dropped background tasks during reload cycles are a real, if development-specific, possibility worth ruling out before assuming a code-level bug.
Still Not Working?
If background tasks need to be genuinely reliable — surviving server restarts, retrying automatically on failure, and being observable/monitorable in a way FastAPI's built-in BackgroundTasks doesn't provide — consider whether a dedicated task queue system (like Celery, RQ, or Dramatiq) is actually a better fit than FastAPI's lightweight, best-effort background task mechanism, which is intentionally simple and doesn't provide the durability guarantees a genuine task queue with persistent storage and retry logic offers for anything beyond simple, non-critical fire-and-forget operations.