Async vs Sync in Python and FastAPI – A Beginner-Friendly Guide with Real Insights

👋 Introduction
If you’re getting started with FastAPI or diving into async/await in Python, you’ve probably asked yourself at some point:
“What’s the point of async?”
“Should I use sync or async?”
“Why are so many FastAPI functions marked asasync def
?”
You’re not alone 😄
In modern backend development—where handling thousands of requests efficiently and reducing response times is key—understanding the difference between synchronous (sync) and asynchronous (async) programming can help you write faster, more responsive, and resource-friendly applications.
In this post, we’ll walk through:
- What sync and async really mean, with simple, real-world analogies.
- How FastAPI natively supports async code.
- When to use sync vs async in your apps.
- The most common mistakes developers make with async (and how to avoid them).
- And finally, we’ll run a mini benchmark to show just how much faster async can be.
Don’t worry if you're not yet familiar with asyncio
, event loops, or concurrency—we’ll break it all down in plain, approachable language, with just enough depth to give you a solid understanding.
Ready? Let’s jump in 🚀
Tuyệt! Dưới đây là phần tiếp theo của bài blog: "Sync vs Async explained simply", giữ nguyên tone thân thiện và dễ hiểu 👇
🔁 Sync vs 🔀 Async – Explained the Simple Way
Let’s start with a simple analogy:
Imagine you're at a coffee shop.
☕ Synchronous (Sync) – Waiting in Line
In a traditional coffee shop, the barista serves one customer at a time.
They take the order, make the drink, hand it over, then move on to the next customer.
That’s how synchronous code works:
- Each task waits for the previous one to finish.
- Simple and predictable—but not very efficient if tasks take time.
import time
def make_coffee():
time.sleep(3)
print("☕ Coffee is ready!")
def sync_barista():
make_coffee()
make_coffee()
make_coffee()
⏱️ In the example above, the barista makes three coffees one after another. Each takes 3 seconds, so you wait a total of 9 seconds.
🔀 Asynchronous (Async) – Place Your Order, We’ll Call Your Name
Now imagine a modern coffee shop:
- You place your order
- The barista starts working on it, while taking new orders
- When your coffee is ready, they call your name
That’s how asynchronous programming works:
- Tasks that take time (like waiting) don’t block everything else
- Instead, they say: “I'll come back when I’m done”
import asyncio
async def make_coffee():
await asyncio.sleep(3)
print("☕ Coffee is ready!")
async def async_barista():
await asyncio.gather(
make_coffee(),
make_coffee(),
make_coffee()
)
⏱️ With async
, all three coffees are being “prepared” at the same time. So the total wait is around 3 seconds, not 9!
✅ Summary
Aspect | Sync | Async |
---|---|---|
Execution | One task at a time | Tasks can “wait” in parallel |
Good for | CPU-heavy, short tasks | I/O-bound, network or file I/O |
Example | time.sleep(3) |
await asyncio.sleep(3) |
Risk | Slows everything down | Needs proper await handling |
Tuyệt vời, mình tiếp tục với phần kế tiếp: "How FastAPI handles Sync and Async" – phần này sẽ bắt đầu đưa người đọc vào thế giới FastAPI một cách nhẹ nhàng nhưng có chiều sâu 👇
⚡ How FastAPI Handles Sync and Async
One of the reasons why FastAPI has gained so much popularity is because it's asynchronous by design.
Unlike older web frameworks like Flask or Django (which are synchronous by default), FastAPI is built on Starlette – a lightweight, high-performance ASGI framework – which means it supports async
out of the box.
✨ So what does this mean for you?
In FastAPI, you can define your route handlers using either:
def
→ for synchronous functionsasync def
→ for asynchronous functions
Let’s see what this looks like in practice:
from fastapi import FastAPI
import time
import asyncio
app = FastAPI()
@app.get("/sync")
def sync_route():
time.sleep(2) # blocking
return {"message": "This is a sync response"}
@app.get("/async")
async def async_route():
await asyncio.sleep(2) # non-blocking
return {"message": "This is an async response"}
🤔 What’s the difference?
- In the
/sync
route,time.sleep(2)
blocks the entire event loop. While it's sleeping, FastAPI can't handle any other requests. - In the
/async
route,await asyncio.sleep(2)
does not block. While it’s waiting, FastAPI can continue serving other users.
📌 Main points:
- Use
def
for simple, fast operations (like in-memory processing). - Use
async def
when you need to wait for something: like a slow database query, an API call, or a file read.
🧪 Want proof? Try it yourself:
- Start a FastAPI app with both routes.
- Hit
/sync
5 times at once → it takes ~10 seconds. - Hit
/async
5 times at once → it takes ~2 seconds.
We’ll walk through this test in the Benchmark section later.
⚖️ When to Use Sync vs Async (and Common Pitfalls)
Now that you know how FastAPI handles both sync and async routes, the next question is:
“When should I useasync def
, and when isdef
just fine?”
The answer depends on what your code is doing.
✅ Use async when your code is I/O-bound
These are tasks where the program is waiting on something external:
- Calling an API
- Querying a database
- Reading a file from disk
- Sleeping (literally
await asyncio.sleep()
😴)
@app.get("/weather")
async def get_weather():
async with httpx.AsyncClient() as client:
res = await client.get("https://api.weatherapi.com/data")
return res.json()
In these cases, async
lets FastAPI handle more requests concurrently, because it’s not stuck waiting around.
🧮 Use sync when your code is CPU-bound
These are tasks that require intensive processing:
- Image processing
- Complex math
- Data transformation in memory
- Looping over huge datasets
Why? Because Python’s async model does not magically run things in parallel — it just handles waiting better.
If you do heavy processing in an async def
, you’ll block the event loop just like you would in sync code.
⚠️ Common Pitfalls (and how to avoid them)
❌ 1. Blocking calls inside async functions
Bad:
@app.get("/bad")
async def bad_route():
time.sleep(5) # Blocking!
Good:
import asyncio
@app.get("/good")
async def good_route():
await asyncio.sleep(5) # Non-blocking!
❌ 2. Forgetting await
when calling an async function
Bad: response = client.get(...)
Good: response = await client.get(...)
You won’t get an error—but the function won’t actually wait, and that can cause bugs.
🧠 Bonus: Calling sync code from async (the safe way)
Sometimes you need to call a legacy or CPU-heavy function from an async
route. If you just call it directly, you’ll block everything.
✅ Use FastAPI’s run_in_threadpool()
to offload sync work safely:
from fastapi.concurrency import run_in_threadpool
def do_heavy_work():
time.sleep(3)
return "done"
@app.get("/safe")
async def safe():
result = await run_in_threadpool(do_heavy_work)
return {"result": result}
This runs your sync function in a separate thread without blocking the async event loop.
⏱️ Benchmark: How Much Faster is Async Really?
Theory is great—but nothing beats seeing the difference for yourself.
In this section, we’ll create two simple FastAPI endpoints:
- One uses sync (
time.sleep
) - The other uses async (
await asyncio.sleep
)
Then, we’ll hit them with multiple requests and compare how long each takes.
🧪 Step 1: Define the endpoints
# main.py
from fastapi import FastAPI
import time
import asyncio
app = FastAPI()
@app.get("/sync")
def sync_route():
time.sleep(1)
return {"message": "sync"}
@app.get("/async")
async def async_route():
await asyncio.sleep(1)
return {"message": "async"}
Each endpoint simulates a 1-second delay.
🚀 Step 2: Run the app
uvicorn main:app --reload
🔁 Step 3: Test with multiple requests
We’ll send 10 requests at the same time using Python.
# benchmark_test.py
import asyncio
import httpx
import time
async def run_benchmark(path: str):
url = f"http://localhost:8000/{path}"
async with httpx.AsyncClient() as client:
start = time.perf_counter()
tasks = [client.get(url) for _ in range(10)]
responses = await asyncio.gather(*tasks)
end = time.perf_counter()
print(f"{path.upper()} - Time taken: {end - start:.2f} seconds")
async def main():
await run_benchmark("sync")
await run_benchmark("async")
asyncio.run(main())
📊 Expected Output
SYNC - Time taken: 10.20 seconds
ASYNC - Time taken: 1.25 seconds
😲 Why the big difference?
- In the sync version, each request waits in line (like a queue at the coffee shop).
- In the async version, all requests wait together using cooperative scheduling.
That’s the real power of async
in I/O-bound tasks.
Mode | Requests | Total Time (Approx) | Efficiency |
---|---|---|---|
Sync | 10 | ~10 seconds | 😴 Slow |
Async | 10 | ~1 second | 🚀 Fast |
📌 Final Thoughts
Async programming in Python may feel intimidating at first—but with FastAPI, it’s easier (and more powerful) than ever.
Once you get comfortable with async def
and await
, you’ll unlock the ability to build faster, more scalable, and modern web applications.
Whether you’re building a hobby project or a production-ready API, using async correctly can make a huge difference in performance 🚀
Thanks for reading, and happy coding! 🙌
Member discussion