Module 4: Background Tasks
Introduction to Module 4: Background Tasks
Overview
Your Task Manager API from Module 3 responds professionally: differentiated schemas, CSV export with streaming, custom headers, complete OpenAPI documentation. But there's one pattern you still don't handle: operations that take time and shouldn't block the response to the client.
Picture this: a user creates a task with POST /tasks. Your API creates the task, saves it, and returns the JSON to the client. But you also want to send a notification email to the team. Sending the email takes 2-3 seconds. Does the client have to wait those extra 2-3 seconds? No. The response to the client should be immediate — "task created, here's the data." The email goes out afterward, in the background. The client never even notices there's an extra process running.
This module teaches you to run operations after returning the response. FastAPI has BackgroundTasks built in for lightweight work. For heavier work, or work that needs retries, scheduling, and monitoring, there's Celery and RQ. You'll learn both levels: what FastAPI gives you natively, and when to scale up to an external task queue.
Where are we in the guide?
You're in Module 4 of 6 of the FastAPI Advanced Features guide:
Module 1: Dependency Injection ✅ (completed)
→ Depends(), sub-dependencies, yield patterns
Module 2: APIRouter and Middleware ✅ (completed)
→ Modular routers, custom middleware, events
Module 3: Advanced Response Models ✅ (completed)
→ Multiple schemas, streaming, custom responses
Module 4: Background Tasks ← YOU ARE HERE
→ BackgroundTasks, patterns, intro to Celery/RQ
Module 5: WebSockets and File Uploads
Module 6: Project — Complete Task Manager API
Cumulative progression
Module 1: DI for DRY code
↓
Module 2: + Modular structure with routers and middleware
↓
Module 3: + Professional responses and streaming
↓
Module 4: + Background processing ← HERE
↓
Module 5: + Real-time communication + files
↓
Module 6: Integration project
Module 3 gave you control over what you return. Module 4 gives you control over what happens after you return.
What you already know vs what's new
What you already know
From Modules 1-3 you bring:
- ✅ Dependency injection with Depends()
- ✅ APIRouter with a modular structure
- ✅ Middleware for logging and timing
- ✅ response_model with differentiated schemas
- ✅ StreamingResponse for large data
- ✅ Custom headers and responses
- ✅ OpenAPI documentation with the responses parameter
What's new in this module
- 🆕 FastAPI's
BackgroundTasks— running functions after the response - 🆕 Adding multiple background tasks
- 🆕 Passing arguments to background tasks
- 🆕 Patterns: fire-and-forget, task chaining, status tracking
- 🆕 Error handling in the background (try/except + logging)
- 🆕 When
BackgroundTasksisn't enough - 🆕 An introduction to Celery with Redis as the broker
- 🆕 An introduction to RQ (Redis Queue)
- 🆕 A comparison: BackgroundTasks vs Celery vs RQ
From "synchronous" to "respond fast, process later"
| Aspect | Before (your current API) | After (this module) |
|---|---|---|
| POST /tasks | Creates task → responds | Creates task → responds → sends email in the background |
| PATCH /tasks/{id} (status=completed) | Updates → responds | Updates → responds → log + cleanup in the background |
| DELETE /tasks/{id} | Deletes → responds | Deletes → responds → audit log in the background |
| Slow operations | Block the client | Run after the response |
| Errors in secondary operations | Fail the main operation | Get logged without affecting the client |
Why background tasks?
Scenario 1: Notifications
WITHOUT background tasks:
POST /tasks → create task → send email (3s) → respond to the client
Total time: 3.2 seconds
The client waits 3 extra seconds for something that doesn't affect them
WITH background tasks:
POST /tasks → create task → respond to the client → send email in the background
Response time: 0.2 seconds
The email goes out afterward without the client ever noticing
Scenario 2: Data processing
WITHOUT background tasks:
POST /reports → generate CSV (10s) → respond to the client
The client waits 10 seconds staring at a spinner
WITH background tasks:
POST /reports → respond 202 Accepted → generate CSV in the background
The client gets immediate confirmation
They can check GET /reports/{id}/status to see the progress
Scenario 3: Cleanup and maintenance
WITHOUT background tasks:
DELETE /tasks/{id} → delete → clear cache → update index → respond
Every extra operation adds latency
WITH background tasks:
DELETE /tasks/{id} → delete → respond → clear cache in the background → update index in the background
Immediate response, cleanup doesn't block
The golden rule
If an operation doesn't affect the result the client receives, do it in the background.
The client calling POST /tasks needs to know whether the task got created. They don't need to wait for the email to be sent. The client calling DELETE /tasks/{id} needs to know whether it got deleted. They don't need to wait for the audit log to be written.
Sync vs async in FastAPI
Before we get into background tasks, let's clear up the difference between sync and async in FastAPI:
# Sync — runs in a thread pool
@app.get("/sync")
def sync_endpoint():
time.sleep(1) # Blocks the thread, but not the event loop
return {"mode": "sync"}
# Async — runs in the event loop
@app.get("/async")
async def async_endpoint():
await asyncio.sleep(1) # Blocks nothing
return {"mode": "async"}
BackgroundTasks works with both:
- Sync functions as a background task → they run in a thread pool
- Async functions as a background task → they run in the event loop
You don't need to make your background tasks async for them to work. FastAPI handles both cases.
Module objective
By the end of this module you'll be able to:
- ✅ Use FastAPI's
BackgroundTasksfor post-response operations - ✅ Add multiple background tasks to a single endpoint
- ✅ Pass arguments to background tasks
- ✅ Implement patterns: fire-and-forget, task chaining, status tracking
- ✅ Handle errors in background tasks (try/except + logging)
- ✅ Understand the limitations of BackgroundTasks (in-process, no retries, no persistence)
- ✅ Know Celery with Redis as the broker (basic setup, the anatomy of a task)
- ✅ Know RQ as a lightweight alternative to Celery
- ✅ Decide when to use BackgroundTasks vs Celery vs RQ
- ✅ Integrate background tasks into your Task Manager API
Prerequisites
For this module you need:
- Module 3 completed — a Task Manager API with professional responses
- Basic Python
logging— we'll use logging to see the output of background tasks - uvicorn running with
--reload
Quick check
cd fastapi-advanced
source venv/bin/activate
uvicorn app.main:app --reload
Check that your Module 3 API works: GET /tasks, POST /tasks, GET /tasks/export/csv.
What if you don't meet one of the prerequisites?
| Missing prerequisite | What to do |
|---|---|
| You didn't do Module 3 | You can still follow this module — the examples are self-contained. But the final project assumes the M3 structure |
| You don't know logging | The capsules explain what you need. import logging; logger = logging.getLogger(__name__) is all it takes |
Module roadmap
| Capsule | Topic | What you'll learn |
|---|---|---|
| 01 | Introduction (this capsule) | Context, why background tasks, sync vs async |
| 02 | BackgroundTasks in FastAPI | Basic usage, multiple tasks, arguments, common patterns |
| 03 | Patterns and error handling | Chaining, dependencies, error handling, status tracking |
| 04 | Celery and RQ — an introduction | When to scale, Redis as the broker, a comparison, a basic example |
| 05 | Project: Background Processing | Email notifications, cleanup of old tasks, status tracking |
Learning flow
First you'll learn the basic mechanism of BackgroundTasks in FastAPI: how to add functions that run after the response, pass arguments, and add multiple tasks (Capsule 02). Then you'll go deeper into advanced patterns: chaining tasks, handling errors without affecting the client, and tracking the state of background tasks with an in-memory pattern (Capsule 03). Next you'll meet the alternatives for when BackgroundTasks isn't enough: Celery with Redis for heavy tasks with retries and scheduling, and RQ as a lighter alternative (Capsule 04). At the end, you'll pull it all together in your Task Manager API: simulated notifications when tasks are created/completed, periodic cleanup of completed tasks, and status tracking (Capsule 05).
The progression is: basic mechanism → advanced patterns → scale when you need to → integrate.
Capsule by capsule
Capsule 02 — BackgroundTasks in FastAPI: The mechanism is simple: you inject BackgroundTasks as a parameter, you call background_tasks.add_task(func, arg1, arg2), and FastAPI runs func(arg1, arg2) after sending the response. You'll implement email simulation, logging, and cleanup with this pattern.
Capsule 03 — Patterns and error handling: Background tasks can fail — and when they do, the client already got their response. So how do you find out? Logging. How do you retry? Manually (BackgroundTasks has no retries). How do you track state? With an in-memory dict that stores the progress. These patterns are the difference between background tasks that work and background tasks you can actually debug.
Capsule 04 — Celery and RQ: BackgroundTasks has limitations: tasks are lost if the server restarts, there's no automatic retry, there's no scheduling, and they run in the same process. Celery solves all of that with separate workers and Redis as the broker. RQ is a simpler alternative. This capsule is conceptual/introductory — not a complete guide to Celery.
Capsule 05 — Project: Your Task Manager API gains background processing: creating a task "sends" an email (a log), completing a task gets recorded in an audit log, tasks completed more than 30 days ago can be cleaned up automatically, and a /tasks/background/status endpoint lets you check the state of background operations.
The mental model: the request lifecycle with background tasks
Without background tasks:
┌─────────┐ ┌─────────────┐ ┌──────────┐
│ Request │ ──▶ │ Endpoint │ ──▶ │ Response │
│ │ │ (all of it) │ │ │
└─────────┘ └─────────────┘ └──────────┘
With background tasks:
┌─────────┐ ┌─────────────┐ ┌──────────┐ ┌──────────────┐
│ Request │ ──▶ │ Endpoint │ ──▶ │ Response │ ──▶ │ Background │
│ │ │ (fast part) │ │ (to the client) │ (slow part) │
└─────────┘ └─────────────┘ └──────────┘ └──────────────┘
↑
The client already has
their response here
The endpoint does the essential work (create the task, validate the data, prepare the response), sends the response to the client, and then runs the secondary operations. The client never waits on the secondary operations.
Connection with the project
Your Task Manager API from Module 3 is going to evolve like this:
Before (Module 3)
POST /tasks → creates task → returns TaskPublic (done)
PATCH /tasks/{id} → updates → returns TaskPublic (done)
DELETE /tasks/{id} → deletes → returns confirmation (done)
No post-response processing
No operation logging
No notifications
After (Module 4)
POST /tasks → creates task → returns TaskPublic → email notification in the background
PATCH /tasks/{id} (→completed) → updates → returns → audit log in the background
DELETE /tasks/{id} → deletes → returns → cleanup in the background
GET /tasks/background/status → the state of background operations
Structured logging of every background task
Error handling that doesn't affect the client
How to use this guide in Module 4
A practical approach
Every capsule includes complete code you should run. The cycle is:
- Read the explanation — understand the concept and the pattern
- Write the code — background tasks require watching the server console
- Watch the console — background task logs show up in the uvicorn terminal
- Check the timing — the response arrives before the background task finishes
- Cause errors on purpose — what happens if the background task fails? Does the client find out?
Estimated time
- Introduction capsule (this one): 15-20 minutes
- Technical capsules (02-04): 30-45 minutes each
- Project (05): 45-60 minutes
- Total for Module 4: 2-2.5 hours
Signs of success
By the end of this module (all 5 capsules), you'll know you succeeded if:
- ✅ You can add background tasks to any endpoint with
BackgroundTasks - ✅ You understand that the response reaches the client before the background tasks finish
- ✅ You know how to handle errors in the background without affecting the response to the client
- ✅ You can implement a status tracking pattern for background tasks
- ✅ You understand when BackgroundTasks is enough and when you need Celery/RQ
- ✅ You know Celery's basic architecture (worker + broker + task)
- ✅ You can choose between BackgroundTasks, Celery, and RQ for a given case
- ✅ Your Task Manager API has notifications and cleanup in the background
Summary
- This is Module 4 of 6, focused on background processing
- The key principle: if an operation doesn't affect the response to the client, do it in the background
- FastAPI's
BackgroundTasksruns functions after sending the response - It works with sync functions (thread pool) and async ones (event loop)
- The limitations of BackgroundTasks: in-process, no retries, no persistence, lost if the server restarts
- Celery and RQ are the alternatives for heavy tasks with retries, scheduling, and monitoring
- The basic pattern: endpoint → response → background processing → logging
- Error handling in the background: try/except + logging — the client never sees the errors
Additional resources
- FastAPI - Background Tasks — The official BackgroundTasks tutorial
- Starlette - Background Tasks — Starlette's documentation (the foundation of BackgroundTasks in FastAPI)
- Python logging — Python's logging module (used to observe background tasks)
- Celery Documentation — A distributed task queue for Python
- RQ (Redis Queue) — A lightweight alternative to Celery
- Real Python - Async IO — A complete guide to async/await in Python
Next capsule: BackgroundTasks in FastAPI — You'll learn to inject BackgroundTasks, add functions that run after the response, and the most common patterns.