Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 84

Asynchronous Request Handling in FastAPI

Recommended resources

Hello! Welcome to the fourth lesson in our module on MLOps and Model Deployment.

In our last lesson, we successfully deployed a machine learning model as a synchronous REST API using FastAPI. While this is a huge step, we ended with a critical question: what happens if a prediction takes a long time? In a synchronous setup, the server is blocked, unable to process any other requests until the long one finishes. This can severely limit the scalability and responsiveness of your application.

Today, we will solve this exact problem. Our goal is to implement asynchronous request handling in FastAPI. We'll explore the concepts of async and await in Python and learn how to leverage them to build highly concurrent, non-blocking APIs. This is a fundamental skill for creating modern, high-performance web services.

1. Synchronous vs. Asynchronous: The Core Idea

At its heart, the difference between synchronous and asynchronous operations is about waiting.

  • Synchronous (blocking): When you make a request, your application stops and waits for the response before doing anything else. It's like waiting in a single-file line at a coffee shop; you can't order until the person in front of you has been fully served.

  • Asynchronous (non-blocking): When you make a request, your application can move on to other tasks while it waits for the response. It's like ordering at a busy restaurant where you get a buzzer; you can go sit at your table and chat while you wait for your food to be ready. The kitchen (the external system) works on your order, and your buzzer goes off when it's done, freeing you up in the meantime.

Asynchronous vs. Synchronous API Request Flow
This diagram illustrates the key difference. In a synchronous model, requests are handled sequentially. In an asynchronous model, the client can send multiple requests and handle other tasks while waiting for responses to arrive independently.

This ability to handle other work while waiting is what makes asynchronous applications so efficient, especially when dealing with many concurrent users making requests that involve I/O (Input/Output) operations like database queries or calls to other APIs.

Asynchronous vs. Synchronous Request Performance Comparison
As this graph shows, an asynchronous endpoint (`/async_ping`) can handle a significantly higher volume of requests per minute compared to its synchronous counterpart (`/sync_ping`), demonstrating the real-world performance benefits.

2. Understanding Asynchronous Concepts

To grasp how this works in Python and FastAPI, let's clarify a few key terms.

Concurrency and async / await - FastAPI

The official FastAPI documentation has a wonderful explanation of asynchronous concepts. We'll start here to build a solid foundation.

Please read the sections "Concurrency and Burgers" and "async and await". The burger analogy is a classic and very effective way to understand the concept of concurrency. Then, the next section will introduce the Python syntax that makes it all possible.

As the documentation explains:

  • Asynchronous code allows a program to pause and switch to another task while waiting for a slow I/O operation to complete.
  • Concurrency is about a single process (like your FastAPI app) managing and making progress on multiple tasks at once, but not necessarily executing them at the exact same time. This is the "burger analogy."
  • Parallelism is about executing multiple tasks at the exact same time, typically on different CPU cores. This requires multiple processes.
  • async def is the syntax used to declare a function as a coroutine—a special function that can be paused and resumed.
  • await is the keyword used inside an async def function to tell the program, "Pause here, wait for this I/O operation to finish, and feel free to work on something else in the meantime."

3. How FastAPI Handles async def

FastAPI is built on this asynchronous foundation and intelligently handles both synchronous and asynchronous code. Understanding how it does this is key to writing efficient applications.

When you define a path operation function:

  • With def (a regular, synchronous function): FastAPI is smart enough to know this function could be slow and blocking. To prevent it from freezing the whole application, it runs the function in a separate thread from an external thread pool. This is a safety mechanism that provides concurrency.
  • With async def (an asynchronous function): FastAPI runs the function directly on the main event loop. This is more efficient as it avoids the overhead of managing threads. However, it comes with a critical rule: you must never call blocking I/O code inside an async def function without await. Doing so will block the main event loop and freeze your entire application.

Let's see this in action.

How FastAPI Handles Requests Behind the Scenes

This short video from Code Collider provides a perfect visual demonstration and technical explanation of how FastAPI processes synchronous and asynchronous requests.

Please watch the entire video (about 5 minutes). Pay close attention to: The difference in output when calling the endpoints using blocking time.sleep vs. non-blocking asyncio.sleep. The explanation of how async def functions run on the main event loop, while def functions are run in a separate thread pool. The final summary of best practices: use async def for non-blocking I/O operations.

This brings us to a crucial point for our work as AI developers.


4. Asynchronous Code in ML Inference

Should you always use async def for your prediction endpoints? Not necessarily. The benefits of async come from waiting for I/O-bound tasks. Many ML model prediction calls (model.predict()) are CPU-bound, meaning they perform intense calculations that keep the CPU busy.

Python's Global Interpreter Lock (GIL) means that even in a multi-core system, only one thread can execute Python bytecode at a time. A CPU-bound task will occupy the CPU and block the event loop, even if it's inside an async def route.

So, when is async useful for ML APIs?

When to Use Async for ML Inference

This article provides a clear answer, distinguishing between the CPU-bound inference step and the I/O-bound steps that often surround it.

Read the entire article. It's short but very important. It explains that async def is most beneficial when your prediction endpoint also performs I/O tasks like fetching features from a database (preprocessing) or logging results (postprocessing).

As the article highlights, an async def endpoint is highly beneficial if your request lifecycle includes I/O operations like:

  • Preprocessing: Fetching user data or extra features from a database before prediction.
  • Postprocessing: Saving the prediction result to a database or sending a notification.

While these I/O tasks are awaiting, the event loop can serve other requests. The CPU-bound model.predict() call itself remains a blocking operation that we will need a different strategy to handle—a topic we'll cover in the next lesson on task queues.


5. Practical Implementation: Converting a Database-Driven API to Async

Now let's put this into practice. The diabetes prediction API from our last lesson didn't have any I/O operations, so converting it to async wouldn't show a real benefit.

A more realistic scenario involves an API that interacts with a database. In Module 2, you learned about SQLAlchemy. We will now see how to convert a standard synchronous SQLAlchemy-based FastAPI application into a fully asynchronous one. This is a common and highly practical task.

This process is more involved than just adding async and await because we need to ensure every part of the stack, especially the database driver and session management, is non-blocking.

Python FastAPI Tutorial (Part 7): Sync vs Async - Converting Your App to Asynchronous

Corey Schafer has an excellent, in-depth tutorial on this exact topic. It's comprehensive, so we'll break it down into conceptual steps. We'll focus on understanding the 'why' behind each change, not just the code itself.

First, watch from the beginning to 04:37 to get an overview of the plan and a recap of sync vs. async concepts.

Step 1: Configure the Asynchronous Database Connection

Our first step is to switch from a synchronous database driver and SQLAlchemy setup to an asynchronous one.

Python FastAPI Tutorial (Part 7): Sync vs Async - Converting Your App to Asynchronous

Let's continue with the video to make the necessary changes in our database configuration.

Watch from 04:37 to 08:28. Focus on the key changes in database.py: Install an async driver: He installs aiosqlite. For PostgreSQL, you'd use a library like psycopg. This is the library that actually performs non-blocking communication with the database. Update imports: Instead of create_engine and sessionmaker from sqlalchemy.orm, you import create_async_engine and async_sessionmaker from sqlalchemy.ext.asyncio. Update the database URL: You add the driver name, e.g., sqlite+aiosqlite:///.... Update the get_db dependency: It becomes an async def function that uses async with to manage the session and yields it.

Step 2: Handle Application Startup with lifespan

Previously, you might have called Base.metadata.create_all(engine) at the top level of your script to create database tables. This is a synchronous call and won't work with our new async engine. The modern FastAPI approach is to use a lifespan context manager.

Python FastAPI Tutorial (Part 7): Sync vs Async - Converting Your App to Asynchronous

Let's see how to set up this lifespan function.

Watch from 10:43 to 13:01. This section explains how to create an async function decorated with @asynccontextmanager that runs code on application startup (before yield) and shutdown (after yield). This is where we will now create our database tables.

Step 3: The Biggest "Gotcha": Eager Loading

This is one of the most critical differences when working with async SQLAlchemy.

  • In synchronous SQLAlchemy, if you access a related object (e.g., my_post.author), SQLAlchemy can automatically run a new query in the background to fetch that author. This is called lazy loading.
  • In asynchronous SQLAlchemy, lazy loading is not supported. Attempting it will cause an error because it would require a blocking database call within an async context.

The solution is eager loading: you must explicitly tell SQLAlchemy which related objects to fetch in your initial query.

Python FastAPI Tutorial (Part 7): Sync vs Async - Converting Your App to Asynchronous

This is a concept that trips up many developers. The video explains it very clearly.

Watch from 13:01 to 14:26. This crucial segment explains why lazy loading fails in an async context and introduces selectinload as the solution for eager loading relationships.

Step 4: Convert the API Endpoints

Finally, we can convert our path operation functions. The pattern is consistent:

  1. Change def to async def.
  2. Change the type hint for the database session dependency to AsyncSession.
  3. Add await before any database calls like session.execute(), session.commit(), or session.refresh().
  4. Add .options(selectinload(...)) to your query statements where you need to load related data.

Python FastAPI Tutorial (Part 7): Sync vs Async - Converting Your App to Asynchronous

Let's see this pattern applied to the first couple of routes.

Watch from 14:26 to 16:30. This shows the conversion of the first two routes. You don't need to watch the entire 8-minute section of him converting every single route, as the pattern is repetitive. Focus on how he applies the four steps mentioned above.

By following this process, you can transform a standard synchronous FastAPI application into a high-performance asynchronous service capable of handling much greater concurrency.


Conclusion

In this lesson, we demystified asynchronous programming in the context of FastAPI. You learned not just how to write async code, but more importantly, why and when to use it.

Key Takeaways:

  • Asynchronous request handling prevents a slow I/O operation from blocking the entire server, dramatically improving concurrency and throughput.
  • FastAPI runs async def routes on the main event loop for efficiency and def routes in a separate thread pool for safety.
  • The async and await keywords are used to define and pause coroutines during non-blocking I/O operations.
  • For ML APIs, async def is most effective for handling I/O-bound pre-processing and post-processing steps around a CPU-bound model inference call.
  • Converting a real application to async requires a full-stack approach, including using async-compatible libraries (e.g., database drivers) and adapting patterns like eager loading.

Preview of the Next Lesson:
We've made our API more responsive to multiple clients, but a long-running CPU-bound task (like a complex model prediction) will still block the event loop while it's actively computing. How do we run a heavy job without making the user wait for an HTTP response? In the next lesson, we will explore task queues using Celery and Redis, allowing us to delegate long-running jobs to background workers and make our API instantly responsive.