How many threads handle one request?
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.
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.