Background Tasks
22 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)A customer places an order. The server must save it and answer quickly. Sending the receipt email can wait a moment. FastAPI lets you hand that slow work to a background task that runs after the response is sent.
Answer first, finish the work after
Background task is a function that FastAPI runs after the response has been sent, so the client does not wait for it. It is meant for short jobs such as sending an email or writing an audit line.
A route that sends an email before it answers makes the customer wait for the mail server. Instead, register the email as a task with BackgroundTasks. FastAPI runs it after the response has gone out.
from fastapi import BackgroundTasks, FastAPI
app = FastAPI()
def send_receipt(email: str): ... # slow work goes here
@app.post("/orders", status_code=201)def create_order(email: str, tasks: BackgroundTasks): tasks.add_task(send_receipt, email) return {"status": "created"}Example 1 · Send a receipt after the response
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPExceptionfrom fastapi.testclient import TestClient
outbox = []app = FastAPI()
def send_receipt(email: str, order_id: int): outbox.append(f"receipt for order {order_id} sent to {email}")
@app.post("/orders", status_code=201)def create_order(email: str, tasks: BackgroundTasks): order_id = 101 tasks.add_task(send_receipt, email, order_id) return {"order_id": order_id, "status": "created"}
client = TestClient(app)res = client.post("/orders", params={"email": "asha@example.com"})print(res.status_code, res.json())print(outbox)201 {'order_id': 101, 'status': 'created'}
['receipt for order 101 sent to asha@example.com']What this code does
- The response only says the order was created. The receipt is a separate step.
TestClientwaits for the task, sooutboxis already filled when we print it.
add_task takes the function first, then the arguments to call it with. Nothing runs yet. The route returns at once.
What the timeline looks like
The order of events is fixed: the route runs, the response is sent, then the tasks run in the order you added them. The picture shows it. Because the client already has its answer, a failing task cannot change that answer.
Example 2 · Several tasks, sync and async
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPExceptionfrom fastapi.testclient import TestClient
events = []app = FastAPI()
def write_audit(action: str): events.append(f"audit: {action}")
async def notify_warehouse(order_id: int): events.append(f"warehouse told about {order_id}")
@app.post("/orders")def create_order(tasks: BackgroundTasks): events.append("route running") tasks.add_task(write_audit, "order created") tasks.add_task(notify_warehouse, 7) return {"ok": True}
client = TestClient(app)client.post("/orders")print(events)['route running', 'audit: order created', 'warehouse told about 7']
What this code does
- The route runs first, then the tasks in the order they were added.
- A task may be a normal function or an
async deffunction. FastAPI runs a normal one in a thread pool so it does not block other requests.
Seeing a task in a test
Task order is the rule that tasks added to one BackgroundTasks object run one after another in the order they were added, each after the response is sent.
TestClient finishes the background tasks before client.post(...) returns. So in a test you can check the effect right after the call. A real client would not wait the same way, but the task code is the same.
client = TestClient(app)res = client.post("/orders", params={"email": "asha@example.com"})print(res.status_code)print(outbox) # the task has already runExample 3 · A task that fails after the response
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPExceptionfrom fastapi.testclient import TestClient
events = []app = FastAPI()
def flaky_email(): events.append("task started") raise RuntimeError("mail server down")
@app.post("/orders", status_code=201)def create_order(tasks: BackgroundTasks): events.append("order saved") tasks.add_task(flaky_email) return {"status": "created"}
client = TestClient(app, raise_server_exceptions=False)res = client.post("/orders")print(res.status_code, res.json())print(events)201 {'status': 'created'}
['order saved', 'task started']What this code does
- The client still gets
201. The order was saved, and the failure came later. - A real server would only log the error. You should catch errors inside the task and record them yourself.
raise_server_exceptions=FalsestopsTestClientfrom re-raising the task error in the test.
Example 4 · Report the job's state: a 202 and a status route
from fastapi import BackgroundTasks, Depends, FastAPI, HTTPExceptionfrom fastapi.testclient import TestClient
jobs = {}app = FastAPI()
def build_report(job_id: int): jobs[job_id] = {"status": "done", "rows": 120}
@app.post("/reports", status_code=202)def start_report(tasks: BackgroundTasks): job_id = len(jobs) + 1 jobs[job_id] = {"status": "pending"} tasks.add_task(build_report, job_id) return {"job_id": job_id, "status": "pending"}
@app.get("/reports/{job_id}")def report_status(job_id: int): if job_id not in jobs: raise HTTPException(status_code=404, detail="Unknown job") return jobs[job_id]
client = TestClient(app)print(client.post("/reports").json())print(client.get("/reports/1").json())print(client.get("/reports/9").status_code){'job_id': 1, 'status': 'pending'}
{'status': 'done', 'rows': 120}
404What this code does
202 Acceptedmeans: we got the request and the work is not finished yet.- The client asks the status route later. A real client could see
pendingfirst.TestClientshowsdonebecause it waits. - This pattern moves to a real task queue unchanged: only the place where the job runs changes.
Test the task's effect, such as a list, a counter or a database row, and not the timing.
Limits, and when to use a real queue
Task queue is a system where the web app puts a job in a stored list (the broker) and separate worker processes take jobs from it, retry failures and survive restarts. Celery and RQ are common Python choices.
BackgroundTasks lives inside your web process. That is simple, but it has limits. It is the right tool for small, quick jobs where losing one now and then is acceptable.
- If the server stops or restarts, waiting tasks are lost. There is no saved list of jobs.
- A failed task is not retried and nobody is told. You must log errors yourself.
- Long or heavy jobs, such as video encoding or big reports, slow down the same process that serves requests.
- For durable, retried or scheduled jobs use a task queue such as Celery or RQ, with a separate worker process and a broker like Redis.
Next step
Check your understanding
11 quick questions