Write blocking, synchronous code before async. Justify the event loop.
A curated, decision-guide list of resources for reaching for synchronous I/O and thread/process-based concurrency before adopting an async runtime — and for recognizing the specific conditions under which async actually earns its complexity.
Licensing note: every software resource below is tagged with its license. This list only curates free/open-source software (FOSS) — no proprietary or SaaS-only products. Where a project has both a FOSS core and a paid proprietary tier, that's called out explicitly.
Async I/O exists to solve one problem well: handling a very large number of concurrent, mostly-idle connections without paying a thread's memory and context-switch cost for each one — the classic C10K problem. That's a real problem, and async is the right answer to it.
But most services never get there. A typical CRUD backend, internal tool, or API gateway serving normal request volumes is I/O-bound in bursts, not idle-holding tens of thousands of sockets at once. For that shape of workload, synchronous code backed by a thread pool or a handful of worker processes is simpler to write, simpler to debug (real stack traces, no await-shaped control flow to mentally unwind), and just as fast in practice. Async also has a well-known "function coloring" cost: once part of your call graph is async, it tends to infect everything that calls it, and mixing sync and async code within one codebase is a common source of subtle bugs (blocking the event loop, forgetting an await, deadlocking a thread pool from inside a coroutine).
This list isn't anti-async — async is the correct default in several ecosystems and for several workload shapes, covered honestly below. It's here to push back on reaching for async as a default before checking whether the workload actually needs it.
- Mostly CPU-bound work, or I/O with low concurrency (a handful to a few hundred concurrent connections) → sync + thread/process pool. The complexity of an event loop buys you nothing here.
- Tens of thousands of concurrent, mostly-idle connections (websockets, long-polling, SSE, huge fan-out gateways) → async is genuinely the right tool; thread-per-connection stops scaling well before this point.
- Team unfamiliar with async debugging (stack traces, cancellation, backpressure) → the operational cost of async is real; weigh it against the concurrency you actually need today, not the concurrency you might need in five years.
- Runtime where async is the foundation, not an opt-in (Node.js, Erlang/BEAM) → the "sync-first" framing applies less; the ecosystem is already built around the non-blocking model, so fighting it is its own complexity tax.
- Go's goroutines are a useful middle ground worth knowing about: cheap, scheduler-managed, and syntactically synchronous — they get you a lot of async's throughput without the
async/awaitcoloring problem.
- Sync-First Web Frameworks & Deployment Models
- Thread & Process Pool Concurrency
- On the Function-Coloring Problem
- Where Async Is the Foundation, Not an Add-On
- When You Actually Need Async
- Further Reading
Frameworks that scale via OS-level workers/threads rather than an in-process event loop.
- Flask
(BSD-3-Clause)+ Gunicorn(MIT)— sync WSGI app, scaled by pre-forked worker processes. - Django
(BSD-3-Clause)— sync views on WSGI workers remain the default and most-used mode, despite async view support. - Ruby on Rails
(MIT)+ Puma(BSD-3-Clause)— thread-pool-based worker model, sync-by-default Rails controllers. - PHP-FPM
(PHP License, OSI-approved)— a process-per-request(ish) model that has quietly scaled a large fraction of the web for decades. - Sinatra
(MIT)— minimal sync Ruby web framework, same worker-based scaling story as Rails.
Primitives for getting real concurrency without an event loop.
- Python
concurrent.futures(PSF License, stdlib)—ThreadPoolExecutor/ProcessPoolExecutor, the sync-first concurrency toolkit in the standard library. - OpenJDK
ExecutorServiceand virtual threads (Project Loom)(GPL-2.0 with Classpath Exception)— cheap, sync-styled threads at scale, narrowing the gap with async runtimes. The OpenJDK reference implementation is FOSS; some vendor JDK distributions carry different, non-FOSS terms — check your specific build. - Sidekiq
(LGPL-3.0 core; Pro/Enterprise tiers are proprietary)— thread-based background job processing for Ruby, sync worker code, no event loop to reason about. - Gunicorn worker model
(MIT, part of Gunicorn)— the design doc is a good short read on why process-based worker pools are a legitimate production architecture, not a stopgap.
- "What Color is Your Function?" by Bob Nystrom — the canonical essay on how
async/awaitsplits a language's functions into two incompatible colors, and why that's a real cost, not just a syntax annoyance. (A written essay, not software — no license tag applies.)
Ecosystems where the "sync-first" framing applies differently, because non-blocking I/O is the baseline rather than an opt-in:
- Node.js
(MIT)— single-threaded event loop by design; fighting it with sync-style blocking calls is its own anti-pattern. - Erlang/OTP
(Apache-2.0)and Elixir(Apache-2.0)— the BEAM's lightweight-process model gives you massive concurrency with code that reads synchronously per-process, arguably the best of both worlds. - Go
(BSD-3-Clause)goroutines — not "async" in theasync/awaitsense, but a scheduler-managed concurrency model that avoids the thread-per-connection ceiling without coloring function signatures.
Be honest about this list too — these are the workload shapes where async is the right call, not a premature one:
- Reverse proxies and API gateways fanning out to many backends per request (nginx
(BSD-2-Clause), Envoy(Apache-2.0)). - Massive websocket/long-lived-connection services (chat, live dashboards, multiplayer, SSE feeds) where thread-per-connection memory overhead becomes the bottleneck.
- High-fan-out I/O orchestration (calling dozens of downstream services concurrently per request) where thread-pool exhaustion is a real risk.
- Ecosystems (Node.js, browser JS) where sync blocking I/O isn't even available as an option.
- The C10K problem — Dan Kegel's original write-up of the concurrent-connections scaling problem async was built to solve.
- "What Color is Your Function?" — Bob Nystrom, linked above, worth its own read outside the function-coloring section.
- Gunicorn design docs — a concrete case study in sync-worker scaling philosophy.
See CONTRIBUTING.md. PRs welcome — especially real-world evidence (write-ups, benchmarks, postmortems) of teams hitting the sync→async threshold in either direction. New resources must be FOSS (or clearly note a license split, like Sidekiq's) — no proprietary-only or SaaS-only submissions.
To the extent possible under law, the contributors have waived all copyright and related or neighboring rights to this work. See LICENSE.
