Async vs Sync in FastAPI
Author: Elliot Otoijagha
Date: Monday 22 June 2026
Subscribe to get new articles delivered straight to your inbox
Async vs Sync in FastAPI
A beginner friendly guide on when you should use either in your FastAPI app
Jun 22, 2026
A little backstory
I'm currently working on a student portal where students pick a cohort to learn from, and tutors give out assignments… (not the main focus here).
As I was writing my first route, I noticed FastAPI could use a sync route. Like any normal person, I probably should've just moved on — but I'm a curious person, I needed to know what's the difference between using an async and sync route. I asked Gemini, read a solid Medium article on it, asked Claude, and once it clicked, I was amazed at how properly FastAPI handles concurrency.
In this article I'll walk through my findings. If you're just starting out with FastAPI, this is for you — I'm just starting out too, so corrections are welcome.
What Asynchronous and Synchronous Programming Mean in Python
Let's use an analogy.
Imagine a chef placing a kettle on the stove to boil water. While waiting, he goes and chops vegetables. Well, that, my friend, is asynchronous programming.
In Python, programs run on one thread. On that one thread, asynchronous programming pauses a task that's waiting on an answer from an external API and lets the thread process the next task at hand. This way, the thread stays non-blocking.
To define an asynchronous function in Python, we use `async def`. To call that function inside another async function, we use `await`.
```python
import asyncio
async def get_weather():
print("Asking the weather API...")
await asyncio.sleep(2) # simulates waiting on an external API
print("Got the weather!")
async def main():
await get_weather()
asyncio.run(main())
You should use asynchronous functions for IO-bound tasks — basically any function that accepts an input, calls an external resource (an API, a database, etc.), and returns an output. That's it.
Synchronous programming flips this. Instead of doing other stuff while the water boils, the chef just stands there and waits for it to finish. Lazy, right?
That's what makes synchronous programming blocking — a new task won't start until the current one is done, even if that current one takes forever. A normal function definition (just `def`, no `async`) is synchronous by default.
```python
import time
def make_coffee():
print("Boiling water...")
time.sleep(2) # blocks here, nothing else can happen until this is done
print("Coffee's ready!")
make_coffee()
Synchronous code is suited for CPU-bound tasks — tasks that lean on the CPU itself: heavy math, image processing, video processing, that kind of thing.
How This Relates to FastAPI
I found that async routes run on the main event loop and handle all their IO-bound operations there. But here's the catch I noticed in the FastAPI docs: if you use a synchronous DB session inside an async route, writing to your DB blocks the event loop — and the thread can't process any other requests until it's done.
At small scale this is negligible, but you'll start to feel it as traffic increases. The fix, especially if you're expecting real traffic, is to use an asynchronous DB session instead.
Here's something I had backwards at first: FastAPI doesn't actually define sync routes as "the CPU-bound ones" — `def` vs `async def` just decides how FastAPI runs your function under the hood. The recommendation to use `def` for CPU-bound work comes from how FastAPI handles it: a synchronous route automatically gets offloaded to a separate thread, so heavy computation (like password hashing) doesn't block the event loop. Write that same CPU-bound logic directly inside an `async def` route without offloading it, though, and it blocks the entire event loop — not just that one request, every other request waiting behind it too.
One caveat worth knowing: running CPU-bound work in a thread isn't true parallelism. Python's GIL means threads still compete for CPU time, so a thread keeps the event loop responsive, but it doesn't make the heavy computation itself faster. For genuinely heavy compute, real parallelism means multiprocessing, not just threads.
For a synchronous function or route, FastAPI spins up a separate thread to handle it (password hashing is a good example), while the main event loop stays free to accept other requests. Smart, right?
The part I wasn't expecting: you can also use AnyIO to run a synchronous function from inside an async route. AnyIO is what FastAPI uses internally for its own threadpool, so using `anyio.to_thread.run_sync` slots your sync function cleanly into that same pool — unlike `asyncio.to_thread`, which spins up its own separate executor.
How I Code Now
After finding all this, I settled on a default architecture that squeezes out as much of FastAPI's performance as possible.
All my routes are asynchronous and use async DB sessions. My services are mostly async too, with a few synchronous ones for heavy computation. My tests are async as well.
So yeah — async-first, top to bottom. It pushes FastAPI to its full capacity.
FYI: async programming can be confusing and a little difficult to write at first, but you'll get the hang of it with time — and you might even fall in love with it.
So yeah, if you made it this far, thanks for reading.
Got questions, corrections, counters, anything at all? I'd love to hear it.
Peace out ✌🏾
Subscribe to get new articles delivered straight to your inbox