Skip to content

About

Write blocking/sync code before async. Justify the event loop. A curated decision-guide awesome-list.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 

Repository files navigation

Awesome Sync-First I/O Awesome PRs Welcome

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.

Introduction

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.

Decision Heuristics

  • 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/await coloring problem.

Contents

Sync-First Web Frameworks & Deployment Models

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.

Thread & Process Pool Concurrency

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 ExecutorService and 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.

On the Function-Coloring Problem

  • "What Color is Your Function?" by Bob Nystrom — the canonical essay on how async/await splits 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.)

Where Async Is the Foundation, Not an Add-On

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 the async/await sense, but a scheduler-managed concurrency model that avoids the thread-per-connection ceiling without coloring function signatures.

When You Actually Need Async

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.

Further Reading


Contributing

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.

License

CC0

To the extent possible under law, the contributors have waived all copyright and related or neighboring rights to this work. See LICENSE.

About

Write blocking/sync code before async. Justify the event loop. A curated decision-guide awesome-list.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors