Background Tasks

Often a request should respond immediately and do slow work afterwards: sending emails, writing audit logs, pushing notifications. BackgroundTasks runs a function after the response has been sent, inside the same process.

Basic Usage

from fastapi import FastAPI, BackgroundTasks

app = FastAPI()

def send_email(email_to: str, message: str):
    # pretend this takes 2 seconds
    with open("emails.log", "a") as f:
        f.write(f"{email_to}: {message}\n")

@app.post("/users/")
async def create_user(email: str, background_tasks: BackgroundTasks):
    background_tasks.add_task(send_email, email, message="Welcome!")
    return {"message": "User created - email will be sent shortly"}

The client gets the 200 response right away; send_email runs afterwards. add_task(fn, *args, **kwargs) accepts the callable plus its arguments.

Async and Sync Task Functions

Using BackgroundTasks in a Dependency

def write_audit_log(background_tasks: BackgroundTasks, user: str = Header()):
    def log(action: str):
        print(f"{user} did {action}")
    background_tasks.add_task(log, "created_item")
    # nothing to return - dependency just schedules side effects

@app.post("/items/", dependencies=[Depends(write_audit_log)])
def create_item(item: Item):
    return item

You can inject BackgroundTasks in dependencies and endpoints alike — tasks from all of them accumulate and run after the response.

Practical Example: Notification With Status Update

async def process_report(report_id: int, db_session_factory):
    async with db_session_factory() as db:
        report = await db.get(Report, report_id)
        report.status = "processing"
        await db.commit()
        # ... heavy work ...
        report.status = "done"
        await db.commit()

@app.post("/reports/", status_code=202)
async def request_report(background_tasks: BackgroundTasks):
    report_id = create_pending_report()
    background_tasks.add_task(process_report, report_id, async_session_factory)
    return {"report_id": report_id, "status": "queued"}

Return 202 Accepted to signal "received, processing later" — the semantically correct status for background work, with a status field the client can poll.

Limitations — know when NOT to use BackgroundTasks: tasks run in the same process that serves the request. If the process crashes or restarts (deploy!), pending tasks are lost. There are no retries, no persistence, no scheduling, and heavy CPU work still competes with request handling. For anything critical (payments, emails that must arrive, large jobs) use a real queue: Celery, RQ, Dramatiq or ARQ with a broker (Redis/RabbitMQ).

Quick Questions and Answers

Q: Does the background task block the response?

A: No. Starlette sends the response first, then awaits the task functions. The client is already gone when the task runs — that is why tasks cannot change the response.

Q: Can a background task access the request's DB session?

A: Not the one from get_db — by the time the task runs, the request lifecycle (and the session) has ended. Create a fresh session inside the task (pass a session factory), or load all needed data before returning the response.

Q: BackgroundTasks vs Celery — how do I choose?

A: BackgroundTasks for fire-and-forget work that may be lost occasionally without harm (logs, non-critical notifications, cache warming) and that finishes in seconds. Celery/RQ for jobs that must not be lost, need retries/timeouts, take minutes, or must run on separate worker machines.

Conclusion

BackgroundTasks is the zero-infrastructure way to answer fast and work later: inject it, add_task(fn, ...), return 202. It has no delivery guarantees — the moment a task matters for the business, move it to a real message queue.