How to Run Background Jobs with Fast API without Blocking the Event Loop
16 Apr, 2026
Introduction
Fast API uses an asynchronous event loop to handle multiple client requests concurrently. When you run long-running blocking tasks like time.sleep() directly inside your endpoint functions or scheduled jobs, you block this event loop. A blocked event loop stops responding to new incoming requests, causing your API to freeze and time out for all users. The AsyncIOScheduler runs jobs inside the same event loop as Fast API, so any blocking operation inside a job halts the entire application.
The BackgroundScheduler solves this problem by running each job in a separate thread, keeping the main event loop free. This guide demonstrates both approaches by comparing AsyncIOScheduler and BackgroundScheduler using simulated long-running tasks with time.sleep().
Prerequisites
Before you start:
- Install Python 3.8 or higher on your development machine.
- Understand basic Fast API concepts like endpoints and event handlers.
Create a Project Directory and Virtual Environment
-
Create a new directory for your Fast API project.
console$ mkdir fastapi-background-jobs -
Move into the new project directory.
console$ cd fastapi-background-jobs -
Create a Python virtual environment.
console$ python3 -m venv venv -
Activate the virtual environment.
console$ source venv/bin/activate
Install Required Packages
-
Install Fast API.
console$ pip install fastapi -
Install Uvicorn.
console$ pip install uvicorn -
Install APScheduler.
console$ pip install apscheduler
Create the Blocking Version with AsyncIOScheduler
The AsyncIOScheduler runs jobs inside the Fast API event loop. When a job calls time.sleep(), it blocks the entire event loop and stops all request handling.
-
Create a new
blocking_app.pyfile with the nano text editor.console$ nano blocking_app.py -
Add the complete blocking application code.
Pythonfrom fastapi import FastAPI from apscheduler.schedulers.asyncio import AsyncIOScheduler import time from datetime import datetime app = FastAPI() scheduler = AsyncIOScheduler() async def blocking_job(): print(f"Blocking job started at {time.strftime('%X')}") time.sleep(30) print(f"Blocking job finished at {time.strftime('%X')}") @app.on_event("startup") def startup_event(): scheduler.add_job(blocking_job, "interval", seconds=10) scheduler.start() print("AsyncIOScheduler started with blocking job.") @app.get("/health") async def health_check(): return {"status": "healthy", "timestamp": datetime.now().isoformat()} -
Save and close the
blocking_app.pyfile by pressing Ctrl + X + Y. -
Run the blocking application.
console$ uvicorn blocking_app:app --reload --port 8000 -
Open a new terminal window and repeatedly call the health endpoint.
console$ curl http://localhost:8000/healthWhen the blocking job runs every 10 seconds, the
time.sleep(30)blocks the event loop for 30 seconds. During this period, the health endpoint does not respond, and your terminal hangs until the sleep finishes.
Create the Non-Blocking Version with BackgroundScheduler
The BackgroundScheduler runs each job in a separate thread. When a job calls time.sleep(), only that thread blocks while the Fast API event loop continues processing requests.
-
Create a new
nonblocking_app.pyfile with the nano text editor.console$ nano nonblocking_app.py -
Add the complete non-blocking application code.
Pythonfrom fastapi import FastAPI from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger import time from datetime import datetime app = FastAPI() scheduler = BackgroundScheduler() async def long_running_task(): # This function is async so can even call other async functions using the await keyword. print(f"Long task started at {time.strftime('%X')}") time.sleep(30) print(f"Long task finished at {time.strftime('%X')}") @app.on_event("startup") def startup_event(): scheduler.add_job( long_running_task, trigger=IntervalTrigger(seconds=10), id="long_task", max_instances=1 ) scheduler.start() print("BackgroundScheduler started with non-blocking job.") @app.on_event("shutdown") def shutdown_event(): scheduler.shutdown(wait=False) print("BackgroundScheduler shut down.") @app.get("/health") async def health_check(): return {"status": "healthy", "timestamp": datetime.now().isoformat()} @app.get("/trigger-task") async def trigger_task(): scheduler.add_job( long_running_task, id=f"task_{datetime.now().timestamp()}" ) return {"message": "Task scheduled in background", "timestamp": datetime.now().isoformat()} -
Save and close the
nonblocking_app.pyfile by pressing Ctrl + X + Y. -
Run the non-blocking application.
console$ uvicorn nonblocking_app:app --reload --port 8000Output:
INFO: Uvicorn running on http://0.0.0.0:8000 BackgroundScheduler started with non-blocking job. INFO: Application startup complete. Long task started at 10:30:00 -
Open a new terminal window and test the health endpoint while the background job runs.
console$ curl http://localhost:8000/healthOutput:
JSON{"status":"healthy","timestamp":"2024-01-15T10:30:15.123456"}The endpoint responds immediately even when the
time.sleep(30)runs in the background thread. -
Observe the terminal running your Fast API application.
Output:
Long task started at 10:30:00 Long task finished at 10:30:30 Long task started at 10:30:40 Long task finished at 10:31:10The background jobs run every 10 seconds, each sleeping for 30 seconds. Despite overlapping executions, the health endpoint remains responsive throughout.
Compare Both Approaches
| Scheduler Type | Runs On | Effect on Event Loop | API Responsiveness |
|---|---|---|---|
| AsyncIOScheduler | Same event loop as Fast API | Blocks entire event loop | API freezes during sleep |
| BackgroundScheduler | Separate threads | No blocking on event loop | API stays responsive |
Conclusion
You have compared both blocking and non-blocking approaches for running scheduled jobs in Fast API. The AsyncIOScheduler blocks the event loop when jobs call time.sleep(), making your API unresponsive. The BackgroundScheduler runs each job in a separate thread, keeping the event loop free and your API responsive. Use BackgroundScheduler whenever your scheduled jobs perform any blocking operations like file I/O, external API calls, sending emails, sending text messages, or even querying remote resources like network router status.