Sync vs async — when each actually wins
Async is not universally faster. It changes the scaling profile: better under high I/O concurrency, worse for single-request latency and CPU-bound work.
The cost model: async has per-operation overhead (coroutine scheduling, state machine transitions, awaitable construction) — often a few microseconds per await. On a workload with 1 DB call and 100 ms wait, this overhead is negligible. On a tight CPU-bound loop with millions of function calls, it can add 20%+ overhead for zero benefit. Profile before assuming async pays off.
Concurrency ≠ parallelism: a single event loop is STILL single-threaded. await asyncio.gather(a(), b(), c()) runs concurrently (interleaved during awaits), not in parallel. Three CPU-bound coroutines gathered don't speed up; they serialize. True parallelism requires multiple processes (multiprocessing, uvicorn --workers N), multiple event loops, or a threadpool for CPU-bound offload.
Mixed sync/async — the right approach: (a) keep I/O paths async end-to-end (async DB driver, async HTTP client); (b) dispatch blocking work explicitly — asyncio.to_thread(blocking_fn) in Python, worker_threads in Node. FastAPI cleverly auto-dispatches def (not async def) endpoints to a threadpool, so you can mix styles in one codebase without blocking the loop.
Async databases are not optional: using a sync ORM (Django <4.1, SQLAlchemy <1.4) inside an async handler defeats the purpose — every query blocks the loop. Real async requires an async driver (asyncpg, aiomysql, motor for Mongo) AND an async-capable ORM layer (SQLAlchemy 2.0 async, Tortoise, Prisma JS, Django 4.1+ limited async ORM).
Python-specific async pitfalls: (a) the GIL still applies — async doesn't parallelize CPU. (b) mixing libraries forces asyncio.run_in_executor or asgiref.sync.sync_to_async wrappers — cognitive tax. (c) traces span many coroutine frames — harder to read. (d) forgotten await bugs (coroutine created but never scheduled) are silent: your code 'returned a Task' instead of a result. (e) cancellation semantics are subtle — a cancelled coroutine may leave locks/resources in an inconsistent state.
Node.js context: Node doesn't offer a 'sync mode'. The event loop IS the model — there's no await-less path. Sync blocking APIs exist (fs.readFileSync, JSON.parse on huge strings) and will freeze the loop — but most modern code uses async libraries. 'CPU work' in Node means worker_threads (child threads running separate V8 isolates), not in-process parallelism.
When to START with async: (i) real-time apps (chat, presence, live dashboards). (ii) API gateways aggregating many upstream calls per request. (iii) long-polling or SSE with many idle connections. When NOT to: simple CRUD with moderate traffic and sync ORM ecosystems — Django sync handles millions of req/day without a single async def.
Grounded on https://www.aeracode.org/2018/02/19/python-async-simplified/
Next up
Full-stack vs API-only vs full-stack JS
Django/Rails (batteries included), Express/FastAPI (minimal), Next.js/Remix (full-stack JS). Each shape optimizes for a different product reality.