Asking whether to write sync or async code used to be a language-level choice. In 2026, every major server-side language supports both — so the choice is now about when async concurrency pays off and when it is just overhead. The core principle has not changed: async exists to handle I/O without blocking; if there is no I/O, async is complexity for no gain.
What changed in 2026
- Python's asyncio matured. FastAPI and async SQLAlchemy make fully async Python stacks practical, not just possible.
- Bun replaced Node.js in some teams' stacks — but the same event-loop rules apply; only the runtime overhead shrank.
- Rust async (Tokio) set performance expectations that pushed other ecosystems to reduce async overhead.
- React Server Components introduced a new async boundary in the UI —
async/await in components is now idiomatic Next.js.
- Structured concurrency (Python 3.11+ TaskGroups, Swift's async let) became mainstream, replacing the footgun of unstructured task spawning.
The fundamental rule
I/O-bound work (database queries, HTTP calls, file reads) should be async. The operation spends most of its time waiting for an external system. Async lets the runtime execute other work during that wait.
CPU-bound work (image resizing, encryption, sorting a large array) should be synchronous (or offloaded to a worker thread/process). Async does not help here — the CPU is busy the whole time, and the async wrapper just adds overhead.
Node.js: why sync blocks everything
// BAD: synchronous file read blocks the entire event loop
const data = fs.readFileSync("large-file.csv"); // blocks for 500ms
// Every other request waits during those 500ms
// GOOD: async read yields back to the event loop
const data = await fs.promises.readFile("large-file.csv");
// Other requests are handled during the file read
Node.js runs JavaScript on a single thread. One synchronous 500 ms operation pauses every in-flight request. At 100 concurrent users, that single blocking call serialises all of them.
async/await patterns
// Sequential (each awaits the previous)
const user = await getUser(id);
const orders = await getOrders(user.id); // must wait for user
// Parallel (both fire at once — use when independent)
const [user, settings] = await Promise.all([
getUser(id),
getSettings(id),
]);
// Fan-out with error handling
const results = await Promise.allSettled([
fetchA(),
fetchB(),
fetchC(),
]);
const succeeded = results.filter(r => r.status === "fulfilled");
Promise.all for independent parallel calls is one of the most impactful async optimisations in any Node.js app.
Python async
import asyncio
import httpx
async def fetch_all(urls: list[str]) -> list[dict]:
async with httpx.AsyncClient() as client:
tasks = [client.get(url) for url in urls]
responses = await asyncio.gather(*tasks)
return [r.json() for r in responses]
Python's async is I/O-bound only — the GIL still prevents true parallelism on CPU-bound code. Use multiprocessing or concurrent.futures for CPU work.
Sync vs async comparison
| Dimension |
Synchronous |
Asynchronous |
| Complexity |
Low |
Higher (event loop, Promise chains) |
| I/O concurrency |
Blocks per call |
Concurrent without threads |
| CPU work |
Natural fit |
No benefit, added overhead |
| Debugging |
Stack traces are linear |
Stack traces can be confusing |
| Error handling |
try/catch, familiar |
try/catch with await, or .catch() |
| Best for |
Scripts, CPU tasks, simple CLIs |
Servers, APIs, real-time apps |
How to pick
- Writing a server that handles concurrent HTTP requests? → Async for all I/O (DB, cache, external APIs).
- Writing a one-off script or CLI? → Sync is fine. No concurrent requests to serve.
- CPU-intensive task (encoding, hashing)? → Sync, offload to worker thread / process pool.
- Multiple independent I/O calls in the same handler? →
Promise.all / asyncio.gather — do not await them sequentially.
- Building a queue consumer? → Async for the message handler, sync is fine for pure computation inside.
Common mistakes
Awaiting in a loop instead of parallelising.
// SLOW: 3 sequential DB calls
for (const id of ids) {
const item = await db.item.findUnique({ where: { id } }); // serial
}
// FAST: all 3 calls fire in parallel
const items = await Promise.all(ids.map(id => db.item.findUnique({ where: { id } })));
Unhandled Promise rejections. Always await or attach a .catch(). An unhandled rejection in Node.js crashes the process in strict mode.
Mixing sync and async in the same abstraction. A function that is sometimes async and sometimes not confuses callers and the type system. Pick one.
CPU work on the event loop. Sorting 1M records synchronously in a Node.js request handler blocks every other request for the duration. Move CPU work to worker_threads or a separate process.
Fire-and-forget without error handling. somePromise() (no await, no catch) silently swallows errors. If you must fire-and-forget, attach at least a .catch(console.error).
What to skip
- Raw callbacks. They were the Node.js pattern of 2010. In 2026, use async/await everywhere. Callbacks create deeply nested, hard-to-read code.
new Promise() wrappers around async functions. Anti-pattern. Just await the async function directly.
- Async for pure computation. A function that does
return a + b does not need to be async. Unnecessary async adds a microtask tick and misleads readers.
FAQ
Does async mean parallel?
No. Async means non-blocking — the event loop can handle other work during a wait. True parallelism requires multiple threads or processes (worker_threads, multiprocessing, Tokio tasks on separate OS threads).
What is the event loop?
A scheduler that runs JavaScript in a single thread. It processes a queue of callbacks/microtasks one at a time. Async I/O returns control to the loop while waiting; synchronous code holds the loop until it finishes.
When should I use workers/threads vs async?
Async for I/O-bound work (network, disk). Workers/threads for CPU-bound work (image processing, crypto). Mixing up the two is the most common Node.js performance mistake.
Is async always faster?
No. For a simple in-memory computation, the async overhead (Promise microtask scheduling) makes it slower. Async is faster only when the alternative would have been blocking on I/O.
Where to go next