Typorithm · medium

BackgroundTasks is deferred process work, not a durable queue

pythonfastapiindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

BackgroundTasks is deferred process work, not a durable queue

0%

Concept and contract

Observe acceptance and execution without claiming crash-safe delivery.

Some work can run after preparing a response: recording a small event or updating an owned cache, for example. FastAPI BackgroundTasks offers a convenient in-process mechanism. It does not turn that work into a durable queue or guarantee completion after a process crash.

The fixture accepts a positive synthetic role ID and schedules an append to an owned list. No email, push notification or external service is contacted. The endpoint returns the number processed when it prepared the response, while the inspection result also reports work observed after the local client completes.

Mechanism and worked example

add_task receives the callable and its arguments instead of the already computed result of calling it.

add_task receives the callable and its arguments instead of the already computed result of calling it. The task runs after response handling reaches the background phase. The local TestClient waits for this ASGI work before returning control to the inspection helper.

For the first request, processed_at_return is zero even though the final done list contains the submitted ID. A second request sees the first completed item before preparing its own response. These observations illustrate ordering in this test runner, not a universal latency promise to a remote client.

Complete worked example

python

from fastapi import BackgroundTasks, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

class Submission(BaseModel):
    id: int = Field(ge=1)

def inspect_example(ids):
    app = FastAPI()
    done = []
    def record(role_id):
        done.append(role_id)
    @app.post("/collect", status_code=202)
    def collect(item: Submission, tasks: BackgroundTasks):
        tasks.add_task(record, item.id)
        return {"accepted": item.id, "processed_at_return": len(done)}
    with TestClient(app) as client:
        responses = []
        for role_id in ids:
            response = client.post("/collect", json={"id": role_id})
            responses.append({"status": response.status_code, "body": response.json()})
    return {"responses": responses, "done": done}
Reasoning
add_task receives the callable and its arguments instead of the already computed result of calling it. The task runs after response handling reaches the background phase. The local TestClient waits for this ASGI work before returning control to the inspection helper. For the first request, processed_at_return is zero even though the final done list contains the submitted ID. A second request sees the first completed item before preparing its own response. These observations illustrate ordering in this test runner, not a universal latency promise to a remote client.

Focused decision: use the complete example for context

python

def record(role_id):
    done.append(role_id)

@app.post("/collect", status_code=202)
def collect(item: Submission, tasks: BackgroundTasks):
    tasks.add_task(record, item.id)
    return {"accepted": item.id, "processed_at_return": len(done)}
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The counterexample calls record immediately instead of registering it.

The counterexample calls record immediately instead of registering it. The side effect then happens before the response snapshot, so processed_at_return is already incremented. All eventual IDs may look correct while the deferred-work contract is wrong.

Do not store essential financial or user-data workflows only in BackgroundTasks and call them guaranteed. A process may stop after accepting a request. Durable delivery needs persistent jobs, retry and idempotency policy, and operational supervision. A background exception also cannot simply replace a response already sent to a client.

Controlled mistake: observe the actual wrong behavior

python

from fastapi import BackgroundTasks, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

class Submission(BaseModel):
    id: int = Field(ge=1)

def inspect_example(ids):
    app = FastAPI()
    done = []
    def record(role_id):
        done.append(role_id)
    @app.post("/collect", status_code=202)
    def collect(item: Submission, tasks: BackgroundTasks):
        record(item.id)
        return {"accepted": item.id, "processed_at_return": len(done)}
    with TestClient(app) as client:
        responses = []
        for role_id in ids:
            response = client.post("/collect", json={"id": role_id})
            responses.append({"status": response.status_code, "body": response.json()})
    return {"responses": responses, "done": done}
Reasoning
The counterexample calls record immediately instead of registering it. The side effect then happens before the response snapshot, so processed_at_return is already incremented. All eventual IDs may look correct while the deferred-work contract is wrong. Do not store essential financial or user-data workflows only in BackgroundTasks and call them guaranteed. A process may stop after accepting a request. Durable delivery needs persistent jobs, retry and idempotency policy, and operational supervision. A background exception also cannot simply replace a response already sent to a client.

Independent practice: one bounded change

Change the accepted response status from 202 to 200 while preserving deferred execution and the response snapshot.

Change the accepted response status from 202 to 200 while preserving deferred execution and the response snapshot. This small exercise focuses on explicit status semantics; explain why a product might prefer 202 for accepted-but-not-yet-completed work before choosing the alternative.

Check one ID, two IDs and no submissions. The final done list should stay the same, while every accepted status uses the new declaration. Do not execute the task early just to justify the new status; this exercise deliberately separates the declared response from task durability.

Step 1: Change the declared accepted status while retaining deferred execution and explaining the semantic tradeoff. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from fastapi import BackgroundTasks, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

class Submission(BaseModel):
    id: int = Field(ge=1)

def inspect_example(ids):
    app = FastAPI()
    done = []
    def record(role_id):
        done.append(role_id)
    @app.post("/collect", status_code=202)
    def collect(item: Submission, tasks: BackgroundTasks):
        tasks.add_task(record, item.id)
        return {"accepted": item.id, "processed_at_return": len(done)}
    with TestClient(app) as client:
        responses = []
        for role_id in ids:
            response = client.post("/collect", json={"id": role_id})
            responses.append({"status": response.status_code, "body": response.json()})
    return {"responses": responses, "done": done}

# Independent task: Change the declared accepted status while retaining deferred execution and explaining the semantic tradeoff.
Reasoning
Change the declared accepted status while retaining deferred execution and explaining the semantic tradeoff.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution changes only the route status.

The solution changes only the route status. Background registration and execution order remain unchanged, and the local client still waits for the background phase. The fixture proves a declared HTTP contract and a local ordering observation; it does not prove crash recovery.

In a real API, 200 and 202 should be selected to match the documented operation semantics. If work completion matters, expose a job state backed by persistent storage and distinguish accepted, running, failed and completed. A changing status code alone cannot add delivery guarantees.

Practice solution and acceptance checks

python

from fastapi import BackgroundTasks, FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field

class Submission(BaseModel):
    id: int = Field(ge=1)

def inspect_example(ids):
    app = FastAPI()
    done = []
    def record(role_id):
        done.append(role_id)
    @app.post("/collect", status_code=200)
    def collect(item: Submission, tasks: BackgroundTasks):
        tasks.add_task(record, item.id)
        return {"accepted": item.id, "processed_at_return": len(done)}
    with TestClient(app) as client:
        responses = []
        for role_id in ids:
            response = client.post("/collect", json={"id": role_id})
            responses.append({"status": response.status_code, "body": response.json()})
    return {"responses": responses, "done": done}
Reasoning
The solution changes only the route status. Background registration and execution order remain unchanged, and the local client still waits for the background phase. The fixture proves a declared HTTP contract and a local ordering observation; it does not prove crash recovery. In a real API, 200 and 202 should be selected to match the documented operation semantics. If work completion matters, expose a job state backed by persistent storage and distinguish accepted, running, failed and completed. A changing status code alone cannot add delivery guarantees.

Interview reasoning and next step

Explain why TestClient returning after background execution does not mean an external client waited for durable completion.

Explain why TestClient returning after background execution does not mean an external client waited for durable completion. Then point out the difference between passing record to add_task and calling record immediately. The response-time count exposes that difference clearly.

The next lesson joins runtime behavior to generated OpenAPI and dependency overrides. Documentation is useful, but neither a declared status nor a test override can replace exercising the real request path and cleaning up test state.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/python-api-lab-20261105.zip. Use an isolated Python environment and the pinned dependencies. Run verify.py --track fastapi, then edit the working exercise baseline and run --practice fa-background. The reference suite rejects meaningful counterexamples and checks values, types and unchanged inputs. All data is owned and all HTTP clients are in-process. No production database, feed, account or paid service is used.

Observe acceptance and execution without claiming crash-safe delivery.