Hello! In our last lesson, we explored how to use BackgroundTasks to improve API responsiveness by deferring non-critical actions. This was our first step into optimizing how our application handles different kinds of work.
Today, we'll dive deeper into FastAPI's core performance feature: its asynchronous capabilities. Your proficiency with Python's async/await will be a great asset here as we connect it to the world of web APIs.
This lesson directly addresses the learning outcome: Distinguish between synchronous and asynchronous route handlers and choose appropriately for I/O-bound tasks. Mastering this is key to building high-performance APIs, especially for services that need to interact with databases, call other APIs, or handle many concurrent users—all common scenarios when deploying AI models.
1. The Foundation: Concurrency vs. Parallelism
Before we look at code, it's crucial to understand two fundamental concepts: concurrency and parallelism. They both sound like "doing things at the same time," but they are different in a way that deeply impacts web application design.
The official FastAPI documentation offers a fantastic analogy involving burgers to explain this.
Concurrency and async / await - FastAPI
To build a strong mental model, please read the following sections from the FastAPI documentation. The 'burger' analogy is a classic and effective way to understand these concepts.
Please read the sections starting from 'Asynchronous Code' up to (but not including) 'Concurrency + Parallelism'. Pay close attention to the 'Concurrent Burgers' and 'Parallel Burgers' stories. Then, read the section 'Is concurrency better than parallelism?'. Focus on understanding the difference between I/O-bound and CPU-bound tasks.
To summarize the key ideas from that reading:
- Concurrency (
async): One worker (e.g., a cashier) juggles multiple tasks. When one task is waiting for something (like a burger being cooked), the worker switches to another task (taking the next order). This is perfect for I/O-bound operations, where the program spends most of its time waiting for a network, a database, or a file. - Parallelism: Multiple workers (e.g., several cooks) work on different tasks simultaneously. This is ideal for CPU-bound operations, where the program is doing intense calculations (like cleaning a house or, more relevant to your goals, processing an image or running a complex ML algorithm).
Web APIs are overwhelmingly I/O-bound. They wait for incoming requests, query databases, and call other services. This is why asynchronous frameworks like FastAPI, which are built for concurrency, are so powerful.
2. How FastAPI Manages async def and def
FastAPI is brilliant because it handles both synchronous (def) and asynchronous (async def) route handlers gracefully. But it does so in very different ways. Understanding this is the key to using them correctly.
async deffunctions are run directly by the main event loop.deffunctions are run in a separate thread pool.
Let's see what this means in practice. The following short video provides a perfect demonstration of the behavior of each type of function.
How FastAPI Handles Requests Behind the Scenes
This video from Code Collider provides a simple, hands-on experiment that clearly shows how FastAPI handles different types of endpoints.
Watch from the beginning to 04:29. Pay close attention to the terminal output in these three experiments: async def with time.sleep(): A blocking call in an async function. async def with asyncio.sleep(): A non-blocking call in an async function. def with time.sleep(): A blocking call in a sync function. The explanation that follows clarifies why you see different behaviors.
As the video demonstrated, using a blocking I/O call (time.sleep) inside an async def function is the worst-case scenario. It freezes the entire event loop, preventing the server from handling any other requests.
This diagram provides a simple visual for the asynchronous path:

The key mechanism is that await in an async def function signals to the event loop, "I'm going to wait for something; you can go do other work now." In contrast, a def function is handed off to a separate thread, so even if it blocks, it only blocks that thread, not the main event loop.
3. Choosing the Right Handler for the Job
Now we can establish clear rules for when to use async def versus def. The choice depends entirely on the type of work being done inside your endpoint.
Asynchronous vs. Synchronous Functions in FastAPI When to Pick ...
This article from Leapcell provides an excellent summary of the decision-making process, with clear rules and examples.
Please read the sections 'When to Use async def vs. def' and 'Conclusion'. This will consolidate everything we've discussed into a practical guide.
Here is a summary of the rules, with examples relevant to your goal of serving ML models.
Use async def when:
Your function needs to await an I/O-bound operation. This is the primary use case.
- Calling external APIs:
import httpx from fastapi import FastAPI app = FastAPI() @app.get("/users/{user_id}") async def get_user_data(user_id: int): # httpx is an async-compatible HTTP client async with httpx.AsyncClient() as client: # We 'await' the network call, allowing the event loop to work on other requests. response = await client.get(f"https://api.example.com/users/{user_id}") return response.json() - Querying a database with an async driver: We did this in Module 5 when we set up SQLAlchemy with
aiosqlite. Every database call wasawaited. - Reading/writing files asynchronously (e.g., with the
aiofileslibrary).
Use def when:
Your function performs CPU-bound work or uses a library that is synchronous (blocking).
- Running an ML model inference: Most ML libraries like
scikit-learnandPyTorchhave synchronous, CPU-intensivepredict()methods. Usingdefallows FastAPI to run this heavy computation in a separate thread, so it doesn't block the event loop from handling other requests.from fastapi import FastAPI # Assume 'model' is a loaded scikit-learn model app = FastAPI() @app.post("/predict") def predict(features: list): # model.predict() is a synchronous, CPU-bound operation. # FastAPI runs this function in a thread pool, protecting the event loop. prediction = model.predict([features]) return {"prediction": prediction.tolist()} - Using a blocking library: If you must use a library that doesn't support
async/await, like the popularrequestslibrary for HTTP calls.import requests # This is a synchronous library from fastapi import FastAPI app = FastAPI() @app.get("/sync-data") def get_sync_data(): # This call blocks, but it only blocks the worker thread, not the main event loop. response = requests.get("https://api.example.com/data") return response.json()
You can freely mix async def and def endpoints in the same application. FastAPI is smart enough to do the right thing with each.
Conclusion
You've now explored one of the most powerful and nuanced features of FastAPI. Understanding when and why to use synchronous versus asynchronous route handlers is what separates a functional API from a high-performance one.
Key Takeaways:
- Concurrency is about efficiently handling waiting (I/O-bound), while Parallelism is about doing multiple things at once (CPU-bound).
- FastAPI runs
async defroutes on the main event loop, making them perfect for non-blocking I/O operations where you can useawait. - FastAPI runs
defroutes in a separate thread pool, which is ideal for long-running CPU-bound tasks (like ML inference) or when using synchronous libraries. - The worst mistake is to perform a blocking operation (like
time.sleepor using a sync library) inside anasync defroute, as this freezes the entire server.
In our next lesson, we will shift our focus to ensuring our application's correctness and reliability. We will learn how to set up a testing environment with pytest and FastAPI's TestClient, enabling us to write automated tests for our endpoints.