The Illusion of Async Everything: Why We Optimized for Waiting Instead of Working
The Illusion of Async Everything: Why We Optimized for Waiting Instead of Working
When we shattered a single machine into five cloud vendors, we didn't make software faster—we just invented a more expensive way to stand in line.
If you step back from the conference talks, the framework wars, and the vendor marketing, and just watch what a web server actually does, it looks like breathing.
Most of the time, the network is silence. A thousand open connections sit quietly on the wire, waiting for a human to click a button or a sensor to report in. Holding a dedicated thread open for silence is like holding your breath—tense, exhausting, and unnatural. Letting the operating system kernel watch those quiet sockets with zero threads—via I/O Completion Ports (IOCP) on Windows or epoll and kqueue on Linux and BSD—is simply letting the machine rest when nothing is happening.
Then a packet arrives. That is the spark.
Once the bytes are actually sitting in memory, ready to be worked on, a strange modern superstition takes over. The modern async/await dogma insists that you cannot simply pick up that request, process it in one unbroken thought, and send the answer back. Instead, you are told to chop a single, continuous human intention—read the request, query the database, build the HTML, flush the socket—into twenty pieces of compiler-generated state-machine confetti.
Why? Just in case a database query takes two milliseconds.
Asynchronous I/O at the network edge is a genuine engineering win, and in .NET, async/await is the right tool to sit on top of IOCP at the socket layer. Yet we allowed viral async Task<T> signatures to infect our entire codebase from the network socket all the way down to the deepest business logic. We ruined the middle to solve a problem that only existed at the edges.
The Boundary Between Waiting and Doing
When you respect the natural physics of the machine, a web request moves through three distinct states:
- The Front (Waiting for the world): Asynchronous. Passive. Nearly weightless. Let the hardware interrupt and the OS kernel tell you when someone knocks on the door.
- The Middle (Thinking and acting): Synchronous. Grounded. One worker thread picks up the task, holds the context in a real call stack, speaks to SQLite or MySQL directly, builds the response, and flushes it out—all in one unbroken stride. If another request is already sitting right behind it in the buffer, it handles that one too before sitting back down.
- The Back (Returning to silence): Hand the socket back to the kernel's watch, release the thread, and return to stillness.
This architecture does not fight the CPU cache by jumping threads four times during a single request, and it does not pollute the programmer's mind with machinery meant for the socket layer. The server handles the waiting at the boundary so the human can think in straight lines in the middle.
Even the wider industry is quietly converging on this realization. Java's virtual threads (Project Loom) and Go's goroutines both allow developers to write plain, straight-line synchronous code while the runtime handles efficient waiting underneath. The lesson they share is simple: straight-line code is what humans are built to write, and waiting is the runtime's job, not the application programmer's.
The Myth of the Sleeping Thread
Whenever you propose keeping the middle synchronous, the same argument always surfaces: "What happens when a request has to wait 1 to 3 seconds in the middle of processing? Don't you need async to free the thread?"
Look closely at what a sleeping thread actually is.
The industry talks about a blocked thread on a two-second wait as if a worker is standing there sweating, burning electricity, and grinding the CPU gears. In reality, a sleeping thread is stillness.
When a thread waits on a database lock, an external payment gateway, or a heavy report, the OS kernel parks it. It consumes zero percent CPU. It generates zero heat. It does not fight the active threads for the processor. What it holds onto is its memory: a call stack whose virtual address space is reserved (1 MB by default on Windows, 8 MB on Linux), but of which only the physical pages actually touched are committed—typically tens of kilobytes.
A sleeping thread is just a quiet bookmark in memory, and a far cheaper one than most people assume.
In a real system, work flows in two completely different rhythms:
- The Heartbeat (99% of requests): Fast queries, page loads, local database reads. Microseconds to a few milliseconds. They fly through, use the thread, and leave.
- The Deep Breaths (1% of requests): Generating a heavy PDF, calling an external bank API, running a massive calculation. That takes 1 to 3 seconds.
If 100 people trigger a two-second wait at the exact same moment, 100 threads sleep—using a few megabytes of committed RAM and 0% CPU—while the remaining threads keep serving fast requests without breaking a sweat.
There is one genuine caveat, and it is the strongest technical argument for async. Default thread pools do not always grow on demand. The .NET thread pool, for instance, injects new threads slowly once it passes its configured baseline. A sudden burst of blocking calls can temporarily starve the pool, leaving fast requests queued behind slow ones. This is a real failure mode, and it is the primary reason ASP.NET Core defaults to recommending async everywhere.
But thread pool starvation is a configuration problem, not a law of physics. You can raise the pool's floor (ThreadPool.SetMinThreads), run a dedicated fixed pool of worker threads sized for your machine, or route known-slow operations to an isolated pool or background queue so they never block the heartbeat. A synchronous middle asks you to size your threads deliberately; in exchange, it gives you code you can actually read, trace, and debug.
Optimizing the Line Instead of the Wait
Suppose we push the hypothetical to the extreme: 1,000 users hit a 3-second bottleneck at the exact same second.
The async evangelist says: "With synchronous threads, only 1,000 requests can wait at once! If you rewrite your entire application in async/await, you free up the threads and your server can accept 50,000 requests!"
Pause and look at what just happened.
Did async make the 3-second work happen any faster? No. Instead of 1,000 people waiting 3 seconds, you now have 50,000 people waiting 3 seconds—or more likely, waiting 30 seconds because you just removed the natural backpressure dam and flooded the underlying database to death.
When a system has a 3-second pause in the middle of a request, the bottleneck is not your capacity to hold people in a waiting room. The bottleneck is the 3-second wait itself.
When that bottleneck is a resource you own—like your database—freeing up web threads so 50,000 requests can pile up against a choking engine is one of the biggest sleights of hand in modern software engineering. A finite pool of synchronous threads acts as a natural circuit breaker; it caps the load on the heart of your system while telling you the truth: fix the slow query, or move the slow job to a background queue. (Async systems can achieve the same protection with semaphores and rate limiters, but they have to bolt it back on manually; a bounded thread pool gives it to you by default.)
When the slow part is someone else's server—such as an external payment provider—the calculation is different. You cannot fix their latency, and holding more waiting connections does not crush your own database. Even then, the cleanest solution is usually architectural: accept the request, hand the slow external call to a background queue, and let the user know the result is on its way.
Why the Modern Web Has So Much Waiting
Async I/O grew out of the C10k problem of the late 1990s, when servers needed to hold ten thousand idle connections at once. Why, then, does a simple web request today spend so much time waiting inside the application logic?
Because we shattered a single machine into five different companies across the internet.
In a self-contained architecture, a user logs in and loads a page:
- The server checks the session in memory or local SQLite/MySQL (~0.2 ms).
- The server reads the application data on the same machine or local LAN (~0.5 ms).
- The server formats the HTML and writes it to the socket.
- Total time: Under 1 millisecond.
Now look at a typical "cloud-native" stack (Next.js + Serverless + Managed SaaS):
- The request hits an Edge router.
- It makes an HTTPS call across the internet to a third-party Auth provider to verify the token (~80 ms).
- It invokes a Serverless Function (if cold start: +500 ms) and makes a network call to a managed Cloud Database in another availability zone (~50 ms to 200 ms).
- It calls a third-party Cloud Storage API or Headless CMS for content (~100 ms).
- It serializes a massive JSON payload and ships it back.
- Total time: Roughly 230 to 380 milliseconds warm (or 700+ ms on a cold start) for a page load that still requires 1 millisecond of actual CPU work.
That is why modern frameworks require async on every function call—because their entire architecture is built around waiting on other people's servers. The architecture created the demand.
The Enterprise Illusion: Can You Just Pay Your Way Out?
We are told that this stitched-together cloud stack is how "modern enterprise software" is built, and that it scales infinitely as long as you pay for the Enterprise Premium tier on every service. While that tradeoff can make sense for a tiny team validating a prototype or serving highly unpredictable global spikes, companies that treat it as the default path eventually hit three walls:
1. The Tax on Every Breath (The Financial Wall)
When you own a server or rent a raw VPS/dedicated machine, a CPU cycle is already paid for. Whether that server handles 10 requests a minute or 5,000 requests a second, your bill at the end of the month is the exact same flat rate.
In the serverless and SaaS ecosystem, you are billed per invocation, per bandwidth gigabyte, per database row read, and per monthly active user. At hobby scale, it is nearly free. At scale, paying the premium tiers routinely costs $20,000 to $100,000+ a month to handle traffic that a pair of well-tuned Linux or Windows servers running compiled C# and a local database could serve at 15% CPU utilization.
This isn't hypothetical. 37signals (the company behind Basecamp and HEY) publicly documented moving off the public cloud with projected savings of millions of dollars over five years. Even Amazon's own Prime Video engineering team famously consolidated a distributed serverless monitoring tool into a single process and cut its infrastructure cost by 90%.
2. The Distributed Failure Chain (The Reliability Wall)
Even if an enterprise has an unlimited budget and pays five different vendors for a "99.9% SLA," basic probability still applies. When a single page load depends on five external cloud services (Edge CDN, Serverless Compute, Cloud Auth, Cloud Database, Cloud Storage) working in sequence, your combined uptime is:
$$0.999^5 \approx 99.5\%$$
That is the difference between roughly 9 hours and 44 hours of downtime a year. When the third-party auth provider has a latency spike on the other side of the world, your login breaks. And because you do not own the machine, you cannot fix it. Your engineering team is reduced to refreshing someone else's status page.
A single self-hosted server is a single point of failure too, of course. The honest comparison isn't "cloud vs. magic uptime"—it is five external dependencies you cannot fix versus a redundant pair of servers behind a load balancer that you actually control.
3. The Connection Pool Trap (The Technical Wall)
Serverless functions spin up and down per request. When a sudden spike ofusers hits the site, the cloud spins up hundreds or thousands of isolated micro-containers. Immediately, those containers try to open fresh TCP/TLS connections to the cloud database at the exact same moment—instantly exhausting the database's connection limit. Warm instances and HTTP-based drivers help mitigate the bleeding, but the vendor's standard solution is still to sell you another paid middleman product (a cloud connection-pooling proxy) to solve the problem that serverless created.
Returning to Physics
The modern cloud-stitching trend was never optimized for machine efficiency or long-term engineering resilience. It was optimized for venture-funded hiring velocity—allowing companies to rapidly onboard developers who glue third-party APIs together without having to manage sockets, threads, memory, or database engines, while sending the bill to the CFO.
Eventually, the companies that survive the hype hit a moment of awakening. First, they get a shocking cloud bill or suffer a major outage across a third-party bridge they cannot repair. Next, they rip out the serverless functions and move their backend onto long-running, stateful servers so connections stay warm and memory stays local.
And finally, they rediscover a simple truth that hardware has known all along:
Keeping your code, your memory, and your data right next to each other on a machine you actually control isn't "legacy." It is physics. Distance costs time, and every network hop is a wait you chose to add. When you understand how the machine works from the socket up, you don't need five middlemen, a labyrinth of async state machines, and a five-figure monthly bill to serve thousands of requests a second. You don't optimize the wait—you remove it.
How does the pacing feel to you now, especially around the .NET thread pool caveat in the middle?