Helm Updates: V1.1 — Synchronous vs. Asynchronous Programming: I/O, Threads and Event Loops
Sync vs Async for IO
When building fast, responsive APIs—you’ll often hear:
“Don’t use a sync calls in an async endpoint.”
But why exactly? What changes between synchronous and asynchronous SDKs, especially for IO-bound operations?
The Problem: IO-Bound Operations and Blocking
Most web backends spend a lot of time waiting on IO—network calls to a database, cache, or external service.
💡 Sync = Standing in a single line at Starbucks, waiting for each coffee before starting the next. Async = Barista starts 5 orders at once, jumps between them as milk boils, espresso shots finish, everybody gets the coffee faster.
Why Does Blocking Matter?
If your server’s threads are stuck waiting on IO, they can’t handle other requests. Scalability collapses as traffic grows (Threads become bottlenecked because each one is blocked, limiting concurrency).
The Event Loop: Async’s Secret Sauce 🥘
The event loop is like a chef managing multiple pans:
Server Activity Timeline
Time →
Req1: [Working][---DB Wait---][Working]
Req2: [Working][---DB Wait---][Working]
Req3: [Working][---DB Wait---][Working]
✔ With Async: while 1 & 2 are waiting, 3 can cook
❌ With Sync: only one line runs at a time
Imagine a server with only one CPU core — meaning it can run just one thing at a time. 5 Requests, 1-Core CPU, Each DB Call Takes 2s
Sync Flow
Async Flow
[Task1 waiting][Task2 waiting][Task3 active][Task4 waiting]
| | | |
<---event loop picks up whatever's ready, no idle time--->
How Sync SDKs Work
A sync SDK issues a call like query(). The calling thread is occupied until the IO finishes.
Example: synchronous Postgres driver (psycopg2)
import psycopg2 # Sync driver
def get_user_sync(user_id):
conn = psycopg2.connect(...)
cur = conn.cursor()
cur.execute('SELECT * FROM users WHERE id=%s', (user_id,))
result = cur.fetchone()
cur.close()
conn.close()
return result
If this runs inside a async endpoint, the thread is stuck until the DB responds. No other coroutine on that thread can run.
Recommended by LinkedIn
How Async SDKs Work
Async SDKs integrate with an event loop (like asyncio). Instead of blocking, they register interest in IO and let the event loop resume the task only when the IO is ready.
Example: async Postgres driver (asyncpg)
import asyncpg
import asyncio
async def get_user_async(user_id):
conn = await asyncpg.connect(...)
result = await conn.fetchrow('SELECT * FROM users WHERE id=$1', user_id)
await conn.close()
return result
Here the await releases control back to the event loop while the database query is in progress. The same thread can handle other requests in the meantime.
The same principle applies to HTTP clients, file I/O, message queues, etc.
OS-Level View: How Async IO Works
When your code does IO, it’s really asking the operating system (OS) to talk to hardware (network card, disk, etc.). The difference between sync and async comes down to how the OS handles readiness:
Putting it together
Async works by:
That’s why a single thread can manage thousands of connections: it’s not spinning on them—it’s only touching the ones that are “ready now.”
Why Sync SDKs Don’t “Become Async”
Calling sync SDKs inside async def doesn’t make them async.
If you must call a sync SDK inside async code, you’d need run_in_executor (a new process or a new thread) to shove the work into a threadpool—but this adds overhead and misses the scalability benefits of async drivers.
"So when should I actually pick sync over async?"
When Sync (Blocking I/O) is Good Enough
When Async (Non-blocking I/O) Shines
👉 Rule of Thumb:
TL;DR ☕
Resources