Hello! Welcome back to our course on FastAPI.
In our first lesson, we established the foundational difference between the Django world and the FastAPI world: the server gateway interface. We saw that Django traditionally uses WSGI (a synchronous standard), while FastAPI is built on ASGI (an asynchronous standard). This difference in the "adapter" between the server and the application is what enables FastAPI's high performance.
Today, we'll move from the server interface level to the application code level. We'll explore how that fundamental difference manifests in the way you write your application logic.
Our learning outcome for this lesson is to: Contrast FastAPI's async-first programming model with Django's synchronous request-response cycle.
We will cover:
- How Django's traditional request-response cycle works.
- The concepts of concurrency and parallelism.
- FastAPI's async-first model using Python's
asyncandawait. - How FastAPI cleverly handles traditional synchronous code, a crucial point for anyone transitioning from Django.
Let's dive in.
The Django Synchronous Model: A Familiar Picture
In Django, you're accustomed to writing views like this:
# A typical Django function-based view
from django.http import JsonResponse
import requests
from .models import Article
def article_list(request):
# The worker thread waits for the database here.
articles = Article.objects.all()
# The worker thread waits for the external API here.
external_data = requests.get('https://api.example.com/news')
# The worker prepares and sends the response.
return JsonResponse({
"articles": list(articles.values()),
"external_news": external_data.json()
})
The key characteristic here is that everything happens sequentially and blocks the worker. When a request for article_list comes in, a Gunicorn worker process (or thread) is assigned to it. That worker executes the function line by line.
- It sends a query to the database. The worker waits for the database to respond. It can't do anything else. This is a blocking I/O operation.
- It sends a request to an external API. Again, the worker waits.
- Once all data is gathered, it constructs and returns the response. Only then is the worker free to handle a new request.
To handle 100 simultaneous users making I/O-bound requests, you'd need roughly 100 worker processes, consuming significant memory. Django has started to incorporate asynchronous support, but its core components, like the default ORM, are synchronous. This makes its async capabilities more of an addition rather than the native, default way of operating.
FastAPI's Async-First Model: Concurrency is Key
FastAPI was designed from the ground up to be asynchronous. This allows it to handle I/O-bound tasks with incredible efficiency through concurrency.
It's important to distinguish concurrency from parallelism:
- Parallelism is doing multiple things at the exact same time. This requires multiple CPU cores. Think of two separate cooks each making a complete meal in their own kitchens.
- Concurrency is making progress on multiple things at the same time, but switching between them. Think of a single cook who puts water on to boil, and while it's heating up, starts chopping vegetables for a salad. They are juggling tasks.
FastAPI leverages concurrency. A single FastAPI worker can juggle hundreds or thousands of requests.
Let's watch a clip that frames this high-level difference and shows why it matters for performance.
FastAPI vs Django - The Ultimate Showdown! (Detailed Comparison)
The video 'FastAPI vs Django - The Ultimate Showdown!' by Eric Roby provides a great overview. We'll focus on the segment that directly compares their performance models and outcomes.
Watch from 02:34 to 06:25. Pay attention to how the video links FastAPI's performance to its async capabilities and ASGI foundation, and note the performance benchmark results shown towards the end.
The performance gains you just saw are a direct result of the async programming model. So, how does this "juggling" actually work in code?
async and await: The Tools of Concurrency
FastAPI uses Python's native async and await keywords to achieve concurrency.
async def: When you define a function withasync def, you are creating a special type of function called a coroutine. It's a function that can be paused and resumed.await: This keyword is used inside anasync deffunction. It tells the event loop, "This operation is going to take some time (e.g., a database query). You can pause me here and go do other work. Let me know when this is done."
Let's explore a fantastic analogy that makes this concept crystal clear.
Concurrency and async / await - FastAPI
The official FastAPI documentation has one of the best explanations of concurrency. We'll read the section that uses a story about buying burgers to differentiate between a synchronous, a parallel, and a concurrent approach.
Read the sections titled 'Concurrency and Burgers', 'Concurrent Burgers', 'Parallel Burgers', and 'Burger Conclusion'. This will give you a strong intuitive grasp of why concurrency is so effective for web applications.
As the article concludes, web servers spend most of their time just waiting—for network requests, database responses, etc. Concurrency, as enabled by async/await, turns that waiting time into productive time spent on other tasks.
Here's what our article_list example might look like in an async world:
# A potential FastAPI equivalent
from fastapi import FastAPI
import httpx # An async-native HTTP client, like 'requests'
app = FastAPI()
@app.get("/")
async def read_root():
# 'await' tells the event loop it can work on other requests while we wait for the DB.
db_results = await some_async_db_driver.fetch("SELECT * FROM articles")
# Same here: await the network call without blocking the whole process.
async with httpx.AsyncClient() as client:
response = await client.get('https://api.example.com/news')
external_data = response.json()
return {
"articles": db_results,
"external_news": external_data
}
The key shift is that the worker is not blocked. While it awaits the database, it can start processing another incoming request or continue a different task that just finished waiting.
The Big Question: What About Synchronous Code in FastAPI?
This is a critical point. Your Django experience is built on synchronous libraries (requests, Django ORM, etc.). What happens if you use them inside a FastAPI endpoint?
Let's consider this:
import time
from fastapi import FastAPI
import requests # A synchronous library
app = FastAPI()
@app.get("/bad")
async def bad_example():
# This is a TRAP!
# 'requests.get' is a blocking I/O call.
# The event loop is stuck here, unable to do anything else.
response = requests.get("https://google.com")
return {"status": "done"}
Using a blocking call inside an async def function is the worst of both worlds. You've told FastAPI this is a coroutine that should play nicely with the event loop, but then you've used a function that blocks the entire loop, preventing any concurrency. The "juggling cook" has frozen, staring at one task and ignoring everything else.
So, how does FastAPI solve this? It gives you two excellent options.
Option 1 (The FastAPI way): Use def for synchronous code
If your endpoint logic involves blocking I/O, simply declare it as a normal function with def, just like in Django:
@app.get("/good")
def good_example():
# FastAPI is smart. It sees this is a normal 'def' function.
# It runs this function in a separate thread from a thread pool,
# so it doesn't block the main async event loop.
response = requests.get("https://google.com")
return {"status": "done"}
FastAPI automatically detects that this is a regular synchronous function and runs it in an external thread pool. This is a brilliant feature that prevents you from accidentally blocking the main event loop and gives you a seamless way to integrate existing synchronous code.
Option 2 (The Async way): Use async-compatible libraries
The "purest" async approach is to use libraries designed for asyncio. For example, instead of requests, you'd use httpx. Instead of the synchronous Django ORM, you'd use an async-capable one like SQLAlchemy with an asyncpg driver (which we'll cover later in the course).
This article provides a practical demonstration of this exact scenario, showing the performance impact of getting it wrong and the simplicity of getting it right.
Navigating the Async Waters: A Practical Guide to Concurrency in ...
The article 'Navigating the Async Waters' gives a hands-on look at this very problem. It benchmarks the different approaches and reinforces how FastAPI manages sync and async functions.
Read the sections 'Where are the async and await keywords?' and the following example where async is added incorrectly. This demonstrates the performance penalty of blocking the event loop and explains FastAPI's solution.
Comparing the Models
Let's visualize how these different approaches fit into a deployment architecture.

Here’s a table to summarize the contrast between Django's traditional model and FastAPI's:
| Aspect | Django's Traditional Model | FastAPI's Async-First Model |
|---|---|---|
| Default Model | Synchronous Request-Response | Asynchronous Event-Driven |
| Concurrency | Via multiple processes/threads (parallelism). | Via a single-process event loop (concurrency). |
| I/O Operations | Blocking. The worker process waits. | Non-blocking. The worker awaits and switches tasks. |
| Route Handler | def my_view(request): |
async def my_endpoint(): (preferred) or def my_endpoint(): |
| Handling Sync Code | This is the native way of working. | def functions are automatically run in a thread pool to avoid blocking the event loop. |
| Mindset | Logic flows sequentially from request to response. | Identify "awaitable" moments to yield control and enable concurrency. |
Conclusion
You've now seen the core philosophical and practical difference in how Django and FastAPI handle requests.
Key Takeaways:
- Django's model is synchronous and blocking: one worker is tied to one request from start to finish. Concurrency is achieved by adding more workers.
- FastAPI's model is asynchronous and non-blocking: a single worker can handle many requests concurrently by "juggling" them, switching between tasks during I/O wait times.
- This is achieved using Python's
asyncandawaitkeywords, which signal to the event loop when a task can be paused. - For existing synchronous code (like the
requestslibrary), FastAPI provides a seamless solution: simply use a standarddeffunction for your route, and FastAPI will run it in a separate thread to avoid blocking the main event loop.
This async-first approach is FastAPI's superpower. It's what allows a FastAPI application to handle a very high number of concurrent connections with minimal resources, making it ideal for building scalable, I/O-bound APIs.
In our next lesson, we will get hands-on and practice this new way of thinking. You will write your own Python coroutines using async and await and run them concurrently using asyncio.gather, solidifying your understanding of how to write asynchronous code.