FastAPI BackgroundTasks: When Fire-and-Forget Is Enough
FastAPI BackgroundTasks lets you run lightweight cleanup, logging, or notifications after the response is sent without delaying the client. Learn when fire-and-forget is enough and when to use a durable job queue instead.
11 Nov 2025, 15:49 UTC

Clients should not wait for work that does not affect the response. Sending a confirmation email, writing an audit log, or invalidating a cache after a POST request can add hundreds of milliseconds of latency if performed inline. The useful takeaway is: FastAPI BackgroundTasks lets you register callables that run after the response is sent, keeping the request fast, but they are in-process and best-effort only.
The post-response problem
An API endpoint often needs two kinds of work: work required to build the response, and work that can happen afterwards. Doing the latter synchronously increases latency and ties up the event loop. BackgroundTasks is FastAPI’s built-in way to defer the second kind until after the HTTP response is on its way.
It is important to note that BackgroundTasks is not a distributed job queue. It is a list of callables attached to a request that FastAPI executes in the same process, in the same event loop, after returning the response.
How BackgroundTasks works
FastAPI imports BackgroundTasks from fastapi.background. You create an instance by adding it as a parameter to your path operation function. You then add callables using background_tasks.add_task(). FastAPI handles the execution once the response has been sent to the client.
Callables can be sync or async. Async callables are awaited in the event loop. Sync callables are run in a separate threadpool to avoid blocking the main event loop, provided they are defined with def rather than async def.
Worked example: create item and log afterwards
Create a file named main.py with the following implementation:
from fastapi import FastAPI, BackgroundTasks
import asyncio
import logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
app = FastAPI()
def write_audit_log(item_id: int):
# Simulate a quick, non-critical write
log.info(f"audit: created item {item_id}")
async def send_welcome_email(item_id: int):
# Simulate I/O bound work
await asyncio.sleep(1)
log.info(f"email sent for item {item_id}")
@app.post("/items/{item_id}")
def create_item(item_id: int, background_tasks: BackgroundTasks):
# Core work that must complete before responding
background_tasks.add_task(write_audit_log, item_id)
background_tasks.add_task(send_welcome_email, item_id)
return {"item_id": item_id, "status": "created"}
Run the application from the project root with a virtual environment activated and fastapi and uvicorn installed:
uvicorn main:app --reloadSend a request using curl:
curl -X POST http://127.0.0.1:8000/items/42Expected checks: The client receives an HTTP 200 response with the JSON body immediately. The server console should subsequently show the audit log line and the email sent line. The response time is not inflated by the 1-second sleep in the email function.
Risk note: Because the task runs in the same process, if send_welcome_email performed heavy CPU-bound computation instead of asyncio.sleep, it could block the event loop and degrade performance for all concurrent connections.
Trade-offs and limitations
BackgroundTasks execute in-process and are not persisted. If the server process crashes, restarts, or the worker is shut down before completion, the pending tasks are dropped silently. Furthermore, exceptions raised within these tasks are logged by FastAPI but cannot be surfaced to the client since the response has already been delivered.
Tasks cannot return values to the client. There is no built-in mechanism for progress reporting or result retrieval. If you need to notify the user of completion, you must use external mechanisms like WebSockets, polling a database, or sending a push notification.
A practical rule of thumb: use BackgroundTasks for lightweight, idempotent, fire-and-forget operations like logging, metrics emission, or cache invalidation. For reliable delivery, automatic retries, or work that must survive a server restart, integrate a durable queue such as Celery, RQ, or a dedicated message broker.
Actionable guidance
- Use
BackgroundTaskswhen the work is quick, non-critical, and acceptable to lose occasionally. - Keep callables small and avoid CPU-bound code.
- Ensure sync functions are defined with
defso FastAPI runs them in a threadpool. - Add explicit logging inside tasks so failures are observable in your logs.
- If reliability is a requirement, enqueue a job to an external broker and return a job ID to the client instead of relying on in-process execution.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.