Skip to main content

Welcome to a Grasp lesson

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

Lesson 4

Async vs. Sync in FastAPI: Handling I/O

Recommended resources

Hello! Welcome back to our FastAPI course.

In our last lesson, you learned the fundamentals of asynchronous programming in Python. We covered how to define coroutines with async def, execute them with await, and run them concurrently using asyncio.gather. The key insight was that asyncio achieves high performance by switching between tasks during I/O-bound waiting periods.

Today, we'll apply that knowledge directly to FastAPI. This lesson bridges the gap between general async Python and building a real API. Our goal is to answer a crucial question you'll face with every endpoint you write.

Learning Outcome: Distinguish when to use async def vs. def for route handlers and explain how FastAPI handles blocking I/O.

This is a fundamental concept that sets FastAPI apart from Django. While Django is primarily synchronous (though it has added async support), FastAPI is built for async from the ground up. Understanding this distinction is the key to writing truly high-performance applications.

The Two Flavors of Route Handlers

In FastAPI, you can declare your path operation functions (the functions that handle requests for a specific URL) in two ways:

  1. As a normal synchronous function, using def.
  2. As an asynchronous function (a coroutine), using async def.
from fastapi import FastAPI
import time
import asyncio

app = FastAPI()

@app.get("/sync-route")
def handle_sync_request():



    # A standard, synchronous function
    time.sleep(1) # Blocking operation
    return {"message": "Hello from a sync route"}

@app.get("/async-route")
async def handle_async_request():



    # An asynchronous function (coroutine)
    await asyncio.sleep(1) # Non-blocking operation
    return {"message": "Hello from an async route"}

Your choice between def and async def is not just stylistic; it tells FastAPI how to execute your code and has significant performance implications. Let's see a practical demonstration of how FastAPI handles these different function types.

How FastAPI Handles Requests Behind the Scenes

The video 'How FastAPI Handles Requests Behind the Scenes' from Code Collider provides a perfect, concise demonstration of how FastAPI executes async def functions with both blocking and non-blocking I/O, and how it handles def functions.

Watch from the beginning to 01:43. The video will show you three experiments: An async def route with a blocking time.sleep(). An async def route with a non-blocking asyncio.sleep(). A def route with a blocking time.sleep(). Observe the output when two requests are made to each endpoint in quick succession. Note which ones run sequentially, concurrently, or in parallel.

As you saw in the video, the behavior is quite different in each case. Let's break down why.


How FastAPI Handles async def

When you declare a route with async def, FastAPI runs it directly on the main event loop. This is the same event loop that is managing all incoming requests and outgoing responses.

  • The Happy Path (Non-blocking I/O): If your function contains an await on a non-blocking I/O operation (like await asyncio.sleep() or an async database query), it yields control back to the event loop. The event loop is then free to work on other things—like handling another request—while it waits. This is how a single FastAPI process can handle thousands of concurrent connections.
Async Route Handling in FastAPI/Uvicorn
This diagram illustrates how a single event loop can manage multiple concurrent requests. When a request performs a non-blocking I/O operation (e.g., `await DB`), the event loop can switch to processing other requests instead of waiting idly.
  • The Pitfall (Blocking I/O): If you use a blocking I/O call (like time.sleep() or the standard requests library) inside an async def function, you block the entire event loop. No other requests can be processed until that blocking call is finished. This completely defeats the purpose of using async and can cripple your server's performance.
FastAPI Async Usage: Avoiding Event Loop Blocking
This image illustrates the performance penalty of incorrectly using a blocking I/O call within an `async def` function. As shown, this misuse can make the 'async' version significantly slower than a correctly handled synchronous one.

How FastAPI Handles def

So what about normal def functions? Does a blocking call in a def route also freeze the server?

No. FastAPI is smart about this. When you declare a route with def, FastAPI runs it in a separate thread from an external thread pool.

This is a crucial feature. It means you can use legacy synchronous libraries or perform heavy CPU-bound computations without blocking the main event loop. The blocking operation happens in a separate thread, allowing the event loop to continue processing other requests.

This model is much closer to how a traditional WSGI server like Gunicorn handles requests for Django, where each request is typically isolated in its own thread or process. FastAPI provides this "safe" synchronous behavior while also offering the more performant asynchronous option.

The official FastAPI documentation has an excellent section explaining this mechanism.

Concurrency and async / await - FastAPI

Let's read the official explanation from the FastAPI documentation 'Concurrency and async / await'.

Navigate to the section 'Very Technical Details', and read the sub-section 'Path operation functions'. It explains exactly how FastAPI runs a def function in an external threadpool to prevent it from blocking the event loop.


The Decision Framework: When to Use Which?

Now we can establish a clear set of rules. The choice depends entirely on what your function does.

Use async def when:

  • Your function is I/O-bound and you are using libraries that support asynchronous operations with await. This is the primary use case.
    • Making HTTP requests with httpx.
    • Querying a database with an async driver like asyncpg or motor.
    • Reading/writing files with aiofiles.
    • Communicating with WebSockets.
  • Your function needs to await another coroutine.

Use def when:

  • Your function is CPU-bound. If your code is doing heavy, in-memory calculations (e.g., complex data analysis, image processing), running it in a separate thread prevents it from hogging the event loop's CPU time. async provides no benefit for CPU-bound work.
  • You are using a library that is synchronous-only. If you need to interact with a database using a blocking driver (psycopg2) or make requests with the requests library, you must wrap this logic in a def route so FastAPI can run it safely in a thread.

The following resource provides a fantastic summary and further examples to solidify this framework.

Asynchronous vs. Synchronous Functions in FastAPI When to Pick ...

The article 'Asynchronous vs. Synchronous Functions in FastAPI' from Leapcell offers a clear, concise guide on this topic.

Read the section titled 'When to Use async def vs. def' for a clear, bulleted breakdown. Then, read the 'Conclusion' for a final summary of the key takeaways.


A Django Analogy

Let's connect this to your experience with Django and DRF:

  • A def route in FastAPI is analogous to a standard Django or DRF view. It runs synchronously. In a production environment with a server like Gunicorn, each request is handled in a separate worker process/thread, so a blocking call in one view doesn't stop others. FastAPI's thread pool for def routes achieves a similar isolation.
  • An async def route in FastAPI is the new, high-performance paradigm. There isn't a perfect one-to-one analogy in the traditional Django world. While modern Django supports async views, it's not the default or core architecture as it is in FastAPI. Think of it as a specialized tool in Django, but the default, foundational engine in FastAPI. The responsibility of not blocking the event loop is the new tradeoff for the massive performance gain.

Conclusion

You have now learned one of the most important architectural concepts in FastAPI. Making the right choice between async def and def is critical for building robust and scalable applications.

Key Takeaways:

  • async def: Use for I/O-bound operations with await-compatible libraries. This runs on the main event loop and enables massive concurrency. Never use blocking I/O calls inside an async def function.
  • def: Use for CPU-bound operations or when you must use a synchronous, blocking library. FastAPI protects the event loop by running these functions in a separate thread pool.
  • The Analogy: Think of def as the familiar, safe Django-style request handling, and async def as the supercharged, non-blocking mode that gives FastAPI its speed.

In our next lesson, we'll put all the pieces from this module together. You will define and run a minimal FastAPI application using Uvicorn, seeing firsthand how to create your first endpoints with the knowledge you've just gained.