3 min read

Async/await’s ease hides a production trap

A new critique argues async/await makes code easy to write but hard to run, especially when I/O and CPU-heavy work collide.

Image: Hacker News

async/await won over developers by making asynchronous code look like ordinary sequential code. But in a recent essay, Philip Banugo argues that this convenience masks deep operational complexity, especially in production systems built on cooperative runtimes such as Rust’s Tokio and Node.js.

His core claim is that asynchrony and concurrency are not the same thing, and async/await blurs the line. In practice, that can turn into a performance trap: a developer writes an async function that fetches data over the network and then performs heavy CPU work — parsing a 10MB JSON payload, traversing a large collection, or running cryptographic code. In a cooperative executor, that work does not yield until it reaches another await, so a 50-millisecond CPU-bound task can stall the entire execution thread and drive up latency for unrelated requests.

Banugo points to a familiar production workaround: split responsibilities across runtimes, using Tokio for I/O and a thread pool such as Rayon for compute-heavy tasks. But he says that fix creates another problem. Teams must manually decide which functions belong in the “I/O pool” and which belong in the “Compute pool,” then maintain message-passing boundaries between the two. He cites postmortems from PostHog Engineering and Meilisearch as examples of how painful that untangling can become.

A second criticism targets default behavior in many async ecosystems. Because calls such as tokio::spawn(…) are cheap and task queues are often effectively unbounded, overloaded systems may continue accepting work instead of applying backpressure. The result, Banugo writes, is a queue that grows until memory runs out and the OS OOM killer terminates the process.

Recommended reading

Europe’s chip push won’t end US cloud reliance

Why work-stealing can hurt at scale

Banugo also takes aim at work-stealing schedulers, which are often presented as a smarter way to balance load across cores. He argues that at very high core counts, fairness can come at the expense of throughput because moving tasks between cores destroys CPU cache locality. He cites Robin Morisset’s account of WhatsApp pushing the Erlang BEAM scheduler on 100+ core machines, where contention over the global runq_lock became a bottleneck.

Project Tina’s thread-per-core model

As an alternative, Banugo introduces Project Tina, an open source thread-per-core, shared-nothing concurrency framework. Instead of async functions, promises, or futures, Tina uses Isolates — units of concurrent work that process messages synchronously and return an Effect.

He describes Tina with a few strict rules:

  • No async/await, Promises, or Futures
  • No work stealing
  • No shared memory or mutexes
  • Pre-allocated memory at process boot
  • Strictly bounded mailboxes

That design, he argues, makes overload explicit: when a mailbox is full, the caller gets immediate feedback rather than silently queueing work until the process crashes. Banugo says the payoff is deterministic behavior, including Deterministic Simulation Testing (DST), where the same seed produces the same execution order every time.

His bottom line is blunt: async/await makes concurrency easier to write, but harder to operate. Tina bets that stricter constraints and visible scheduling are a better trade-off than runtime magic.

Marcus Vance

Enterprise Editor

Marcus follows the money. He covers enterprise software, cloud architecture, and the tectonic shifts in Big Tech strategy. He translates dense earnings calls and complex M&A activity into actionable insights about where the industry is actually heading. If a tech giant makes a silent pivot, Marcus is usually the first to notice.

via Hacker News

// Keep reading