How many threads handle one request?

1 min read

At any instant, one thread works on a request. Over its lifetime it may use several. Each synchronous slice between awaits runs on a pool thread. At an await, the thread returns to the pool, and the continuation runs on whichever thread is free.

C#
public async Task<IActionResult> Get(){    var user = await _db.GetUserAsync(id);         // (A) runs on pool thread #7    var orders = await _http.GetOrdersAsync();     // (B) resumes on pool thread #12    return Ok(new { user, orders });               // (C) resumes on pool thread #3}

CPU work in the handler

It needs no Task.Run: the handler is already on a pool thread (see Task.RunTask.Run: when it helps and when it doesn'tTask.Run moves CPU work to a pool thread. It pays off when the current thread must stay free.Opens in a new tab).

How a small pool serves many requests

A thread is occupied only during CPU slices, not I/O waits. If a request is 95% awaiting a database and 5% computing, one thread can time-share about 20 concurrent requests. Kestrel handles tens of thousands of connections with a pool in the hundreds (see KestrelKestrelThe web server inside every ASP.NET Core application.Opens in a new tab).

“Hundreds of millions of requests” is throughput over time. Concurrent requests are bounded by cores, memory and sockets. Past one machine, scale out behind a load balancer.

Where it matters

Server threads are scarce and shared: async I/O gives them back to the poolThe thread poolA set of reusable worker threads that take jobs from a queue.Opens in a new tab. See heavy work in a requestHeavy work inside an HTTP requestCores are shared by every request, so one request that takes them all slows the others.Opens in a new tab and DI scopes and threadsDI scopes, threads and processesA scope is a logical unit of work, not a thread or a process.Opens in a new tab.