skip to content

A server that dedicates one operating-system thread to each open connection behaves fine at a few hundred clients but collapses somewhere in the tens of thousands. Concretely, what runs out, and why did that problem drive servers toward event-driven designs?

level: middleimportance: must knowfreq 55%

answer

  1. cost scales with connections, not work
  2. 1 MB stack x 10k threads
  3. switch cost > 200 bytes of parsing
  4. idle keep-alives still pay full price
  5. user-space threads change the arithmetic, not the principle

basics

~20 s

Each thread costs reserved stack memory, a kernel scheduling entity, and cache footprint. At 10k+ mostly idle connections you pay gigabytes of stacks and constant context switching for almost no work, so throughput falls while connections rise. Event loops decouple connection count from thread count.

solid answer

~50 s

Thread-per-connection ties a scarce resource (an OS thread) to an abundant one (an idle socket). Three things run out. 1. **Memory** — each thread reserves stack address space, historically ~0.5–8 MB; 20,000 threads is tens of gigabytes of reservation and a large committed footprint. 2. **Scheduler and switch cost** — every thread is a schedulable entity. Waking thousands of threads that each read 200 bytes means the machine spends its time saving and restoring register state and walking run queues instead of serving requests. 3. **Cache and TLB locality** — each switch drags in a different stack and working set, so hit rates drop and effective instructions-per-cycle falls even when CPU looks busy. The key insight of the C10K problem is that **the cost scales with connections, not with traffic** — 10,000 idle keep-alive connections cost nearly as much as 10,000 busy ones. Event-driven servers make cost scale with *events*: a handful of threads multiplex all sockets, so memory per connection drops to a small buffer plus state.

go deeper

for a junior

Say that each connection holds a thread, each thread costs stack memory and scheduling, and that idle connections still pay — so the machine runs out of memory and burns CPU switching.

for a middle

Give the numbers (MB-scale stacks, microsecond switches), state that cost scales with connections rather than work, and describe the event-loop alternative and its state-machine cost.

for a senior

Add cache/TLB locality and contention effects, discuss where the loop itself becomes the bottleneck, and note that handlers must never block.

for a principal

Position it as a resource-model decision — what should concurrency cost be proportional to — and evaluate lightweight-thread runtimes, per-core sharding, and operational cost of debuggability against raw efficiency.

## The shape of the problem "C10K" is the shorthand for the observation, popularised around 1999–2003, that hardware of the day could easily push the bandwidth for ten thousand simultaneous clients, yet server *software* fell over long before that. The bottleneck was architectural, not physical: the dominant server model gave every connection its own thread (or process), and blocked that thread on every read. That design is beautifully simple. Each connection gets a straight-line procedure with local variables, a natural call stack, and ordinary sequential control flow; the operating system handles the interleaving. It fails only for one reason: it makes the unit of cost the *connection*, when the thing you actually want to pay for is the *work*. ## What actually runs out **Stack memory.** A thread needs a contiguous stack, sized for the deepest call chain it might ever take — historically a megabyte or more of reserved address space, with pages committed as touched. Ten thousand threads is a stack budget measured in gigabytes of reservation, and even the committed fraction (guard pages, top frames, thread-control structures) is hundreds of megabytes doing nothing. Modern connections are mostly *idle*: a keep-alive HTTP connection or a WebSocket may transfer nothing for minutes while holding its stack the whole time. **Scheduling entities.** Every OS thread is an object the kernel must track and place on run queues. Bookkeeping that is trivial at hundreds becomes significant at tens of thousands, and any operation proportional to the number of threads (accounting, signal delivery, some debugging or profiling paths) degrades. **Context-switch throughput.** This is the killer at scale. A switch costs on the order of a microsecond of direct work — save registers, swap stacks, change address-space bookkeeping — but the *indirect* cost is worse: the new thread's stack, heap objects and instruction footprint are cold in L1/L2 and in the TLB. When each wake-up does 200 bytes of parsing, the switch overhead can exceed the useful work by an order of magnitude. The machine reports high CPU utilisation while doing very little; throughput plateaus and then declines as concurrency rises, and latency variance explodes. **Thundering herds and lock convoys.** Thousands of threads contending for an accept path, a shared cache, or an allocator turn every shared structure into a contention point. Even with fine-grained locking, the sheer number of runnable threads amplifies queuing effects. ## Why event-driven designs fix it An event-driven server keeps a small number of threads — often one per core — each running a loop: ask the kernel which of the registered handles are ready, then run a short handler for each ready handle. Per connection you now store only a buffer and a small state object (a few kilobytes at most, sometimes a few hundred bytes), and you pay CPU only when bytes actually move. Cost scales with *events per second*, not with *connections*. Ten thousand idle connections become ten thousand entries in a kernel data structure and ten thousand small state records — no stacks, no scheduling entities, no switches. The trade is a real one. Straight-line code becomes a state machine: you cannot "just call read and wait", so control flow is chopped into callbacks, promises, or resumable coroutines. Any handler that blocks or runs long stalls every other connection on that loop, because there is no preemption inside a handler. Debugging is harder because a stack trace no longer shows the whole logical operation. These are the costs event-driven servers pay to make connections cheap. ## The modern qualification The arithmetic changes when threads stop being OS threads. Lightweight user-space threads — green threads, goroutine-style schedulers, fibers, virtual threads — give each connection a small growable stack (kilobytes, grown on demand) and multiplex many of them onto a few carrier threads, switching in user space without a kernel transition. Underneath, the runtime is still doing readiness-based multiplexing; it just hides it, restoring blocking-style code with event-loop-style costs. So the *pattern* thread-per-connection is no longer disqualifying — but thread-per-connection **on kernel threads** still is, and the reason it was disqualifying (cost tied to connections, not work) is exactly the reason those runtimes exist. ## Numbers worth carrying - OS thread stack reservation: often ~1 MB default, sometimes 8 MB; user-space thread: a few KB, grown on demand. - Context switch: roughly 1–10 microseconds all-in including cache effects; at 100,000 switches/second that is a meaningful fraction of a core. - Per-connection state in an event-driven server: typically a read buffer plus parser state — hundreds of bytes to a few KB. ## How to say it in an interview Name the resource (stacks, scheduling entities, switches, cache locality), state the scaling rule (cost per *connection* rather than per *unit of work*), and then say what event-driven buys and what it costs (state machines, no blocking allowed in handlers).

  • Ten thousand connections are open but only fifty are transferring data at any instant. How does each architecture's cost behave?
    Thread-per-connection pays nearly the full price for all ten thousand: every idle connection still holds a stack and a scheduling entity, even though it consumes no CPU. An event-driven server pays for the fifty active ones plus ten thousand cheap state records and kernel registrations. That gap — cost tied to connections versus cost tied to events — is the entire C10K argument.
  • If lightweight user-space threads make blocking code cheap again, is the event-driven model obsolete?
    No — it moved underneath. Those runtimes still register non-blocking handles with a readiness mechanism and run a scheduler loop; they simply present a blocking API over it. What changed is the developer-facing trade: you can write sequential code again. Explicit event loops remain preferable where you need precise control of scheduling, buffer reuse, or per-core sharding with no synchronisation.

Thread-per-connection is hiring one waiter per table and paying them whether or not the table has ordered. An event loop is one waiter who walks the room and stops only where a hand is raised.

saying these in an interview costs you the question

  • Blaming "too many context switches" without naming stack memory or the connections-versus-work scaling rule.
  • Claiming an event loop is faster per request — it is not; it is cheaper per idle connection.
  • Saying idle connections are free in a thread-per-connection server.
  • Proposing a bigger thread pool as the fix for 10k connections.
  • Confusing kernel threads with lightweight user-space threads when quoting stack sizes.

context