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.
A common myth: 'async = fast, sync = slow'. Reality: async changes the SHAPE of how your server scales, not the raw speed of one request. Often async is slightly SLOWER per request, but handles far more concurrent requests.
The mental picture: sync = 'I'll block here until done'. async = 'I'll go work on something else while I wait'. For ONE request, sync is simpler and usually faster by a tiny margin. For 1,000 concurrent requests that each wait 100 ms on a DB, async wins by orders of magnitude because you're not paying for 1,000 threads.
When async wins: many concurrent I/O-bound operations (fetching 20 upstream APIs per request, chat with thousands of idle sockets, long-polling, server-sent events). You need concurrency, not parallelism.
When sync wins: CPU-bound work (image processing, ML inference, cryptography), single-request latency sensitivity, mature sync-only library ecosystems (Django's ORM + admin, most Python data-science libs). Async adds real overhead per call; if you don't need the concurrency benefit, skip it.
The #1 bug: calling a sync blocking function inside an async handler. time.sleep, requests.get, a sync ORM query — they all freeze the entire event loop until they finish. Every other pending request in the process waits. Easy to write; hell to debug.
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.