← Home
.NETAsync and threadingReported by candidates

How does async/await work, and why use it in a web API?

Asked at Deloitte, IBM, Infosys, Wipro, and TCS, and by .NET interviewers on r/dotnet.

Say this first · about 30 seconds
Make it yours · UDC Finance · Commercial vehicle lending portal
The code, simplified

The concurrent valuation service at UDC Finance, simplified.

UDC.sln · where this file sits
  • src/Services/

Fix later in your repo Invented example: production UDC code wraps both HTTP clients in Polly resilient rate-limiting pipelines.

Say it out loud, then check

Show the 30-second answer
  • In a web API, async and await are about server throughput and scalability, not making a single request run faster.
  • When an action awaits an I/O call like a database query or external service, it yields the thread back to the thread pool so other incoming requests can use it.
  • Under the hood, the compiler rewrites the method into a state machine that pauses execution at incomplete awaits and saves local state.
  • It hooks a continuation callback to the pending task and returns to the caller, so zero threads sit idle during network waits.
  • When the operating system signals that the I/O data has arrived, the thread pool picks up the continuation and resumes execution from that exact step.
Check your answer: tick what you said

You said 0 of 8

Then add your example: UDC Finance · Commercial vehicle lending portal
  • At UDC Finance, commercial loan approval requires valuation checks against two vehicle valuation APIs.
  • An older shared helper invoked HTTP calls synchronously with .Result, causing thread-pool starvation during NZ morning peak.
  • I refactored the pipeline to async end to end and orchestrated the checks concurrently using with cancellation tokens.
  • Peak p95 latency dropped from 8.4 seconds to 410 ms, and ThreadPool queue length stayed flat at zero.

See it

1/7Request arrives. Thread 1 picks it up from the pool and starts executing the controller.

In plain words

The senior answer · about 2 minutes
Why use it in a web API
  • I explain async in ASP.NET Core in terms of server throughput, not request latency.
  • Kestrel serves requests using the .NET . If an endpoint does blocking I/O, that thread sits idle waiting on network packets or database responses while doing zero CPU work.
  • Under peak load, hundreds of concurrent blocked calls exhaust the pool. Because the thread pool only injects one or two threads per second once minimum threads are passed, incoming requests queue up and the whole API stalls.
  • Async releases that thread immediately upon awaiting I/O, allowing a handful of threads to serve thousands of concurrent requests.
How the compiler state machine works
  • Under the hood, the compiler rewrites an async method into a private struct implementing .
  • Local variables and parameters are hoisted into fields on the struct so their state survives across suspension points.
  • The method's entry point creates an and calls . If the awaited is already completed, execution continues synchronously on the fast path with zero allocations.
  • If the task is incomplete, the builder moves the struct to the heap, registers as the continuation callback via the awaiter, and returns the builder's Task to the caller.
  • When the operating system I/O completion port or epoll fires, the thread pool invokes , jumping straight back into the switch statement at the next state index.
ASP.NET Core and SynchronizationContext
  • In older .NET Framework and desktop apps, forced continuations back onto a specific UI thread or request thread, which caused deadlocks with sync-over-async.
  • ASP.NET Core deliberately has no . Every continuation resumes on whatever thread-pool thread is free.
  • That means ConfigureAwait(false) is not needed in controller or service application code, though I still use it in shared Class Libraries so they remain safe if consumed by a desktop client.
Production patterns and pitfalls
  • I enforce async all the way down: controller to service to repository. A single sync-over-async call like .Result or .Wait() inside the chain destroys the scalability benefit.
  • I ban in code reviews and analyzers because unhandled exceptions cannot be caught and terminate the process.
  • I always pass down to EF Core and so that when Kestrel detects a client disconnection through , expensive backend work stops immediately.
  • For hot paths where data is often cached, I consider to avoid allocating Task objects on the heap.

Inside the compiler: the async state machine

When you write async and await, the C# compiler transforms your method into a state machine struct implementing IAsyncStateMachine.

1/6Method called. The struct initializes with state -1. MoveNext begins synchronously.

The story

  1. Before

    In synchronous code, a thread pool thread is assigned to the request when it enters (ASP.NET Core's web server). When that thread calls a database query or an external API, it sits blocked doing nothing until the network packets return. That thread cannot do any other work.

  2. Why it had to change

    Under heavy traffic, hundreds of requests hit blocking I/O simultaneously. The thread pool runs out of available threads, known as thread-pool starvation. New requests pile up in the thread pool queue, and latency surges across the entire server, even on simple in-memory endpoints.

  3. How it works now

    With and , the method is split at the suspension point. When your code awaits an incomplete , the calling thread is returned directly to the thread pool to process other incoming traffic. No thread is held while waiting for network or disk I/O.

  4. What happens next

    When the database driver receives the response packets from the socket, the operating system notifies the .NET runtime. The runtime queues a continuation (the callback that executes the remaining code) to the thread pool, and any available thread picks up execution right after the await statement.

  5. What we gained

    Server throughput (the number of concurrent requests served per second) multiplies by orders of magnitude. A small pool of 10 to 20 threads can easily handle 5,000 concurrent I/O-bound requests without exhausting server memory or stalling response times.

In the lending API

The lines that matter.

Only the key lines are shown; Deep walks the whole file.

Lending.sln · where this file sits
  • src/Api/Controllers/Servicing/
⋯ 2 lines in Deep⋯ 4 lines in Deep
LoansController.cs · line 11 · 1 of 3Takes CancellationToken from HttpContext.RequestAborted to cancel queries if client disconnects.

The code, file by file

In ASP.NET Core, routing directs an incoming HTTP request to LoansController.GetByIdAsync, injecting services from DI (ILoanService, ILoanRepository, ServicingDbContext). Stepping through shows the execution flow down the call stack and back up.

Lending.sln · where these 3 files sit
  • src/
    • Api/Controllers/Servicing/
    • Application/Servicing/Loans/
    • Infrastructure/Servicing/
Diagram · step 1Request arrives. Thread 1 picks it up from the pool and starts executing the controller.
LoansController.cs · line 11 · 1 of 3Takes CancellationToken from HttpContext.RequestAborted to cancel queries if client disconnects.

What they ask next

The follow-ups interviews really ask. Answer out loud, then tap to check.

Follow-up tree

Answer, then open the next level. That is the order an interviewer digs in.

Reporteda candidate was asked Intervieweran interviewer asks it

Production bugs, and how to tell them

Each one: what you see, why, the fix in code, and the line to say.

Production incident: Morning rollout stalled the dealer API

P1 · outage or data leakUDC Financeonly under loadDealer loan origination API

Composed, not a real event at UDC Finance. Every mechanism is real; the stakes and diagnostic signals are modelled on production thread-pool starvation incidents: Microsoft Learn: Diagnosing .NET ThreadPool Starvation

The story in 5 lines
  1. At the New Zealand morning peak during UDC's wider rollout, every endpoint across the dealer portal API slowed to 14 seconds, including the health check.
  2. CPU was low at 30% and SQL was fast, but showed ThreadPool thread counts climbing and thread queue length spiking into the hundreds.
  3. A shared document helper called an external FIS document API using synchronous .Result inside loan disclosure requests.
  4. Forty concurrent disclosure calls held forty threads blocked on I/O, starving the ThreadPool, which injects only one to two new threads per second.
  5. We mitigated by toggling off on-demand PDF generation, then shipped an end-to-end async fix with background queuing and an analyzer banning .Result.
Who's who (4 people)
You
.NET developer on vendor team (L3). Owned the disclosure flow and async refactoring.
Sushmita
Backend lead in Navi Mumbai. Ran the bridge call and reviewed the emergency PR.
Nitin
Platform engineer. Scaled the App Service and monitored thread metrics.
Arman
UDC IT operations manager in Auckland. Chaired the P1 incident bridge.

Tap a step. The map lights the place where it happens.

1/8 Detect

Dealers report frozen screens. Latency jumps to 14 seconds across all endpoints.

Tue 28 Apr 2026, 10:30 NZST = 04:00 ISTYou (on-call L3) and Arman

  • At 10:30 Auckland time, New Zealand vehicle dealers log in to submit applications. Within five minutes, portal requests crawl from 700 ms to 14 seconds.
  • Even lightweight health-check calls (/healthz) time out. Azure autoscale kicks in and adds two instances, but latency remains above 12 seconds.
  • An automated p95 alert (the latency below which 95% of requests finish) pages you at 04:05 IST. You immediately join the emergency opened by Arman and page backend lead Sushmita.
App Insights Alert: High p95 Server Latency
SEV-1 ALERT: Server Response Time > 5.0s
Resource: app-udc-dealers-prod
Metric: requests/duration (p95) = 14.2s
Health check: /healthz 503 Unhealthy
Active instances: 4 (autoscale maxed)
In theory

Autoscaling handles sudden bursts by spinning up additional instances to share the request load.

In practice

Autoscale added instances, but each new instance was crippled by the exact same sync-over-async blocking as soon as requests hit it.

Try it yourself · 15 minutes, on your Mac
  1. dotnet new webapi -n ThreadStarveLab and set in Program.cs to simulate a constrained pool.
  2. Add an endpoint app.MapGet("/sync-blocking", () => Task.Delay(3000).Result) that blocks with .Result.
  3. Add a fast endpoint app.MapGet("/fast", () => "ok").
  4. Fire 10 concurrent requests to /sync-blocking using curl or Bombardier, then hit /fast: notice /fast hangs for seconds waiting for a thread.
  5. Replace .Result with await Task.Delay(3000) and repeat: /fast responds instantly in 2 ms even under 100 concurrent requests.
You should see

Sync-over-async locks up the entire web server; pure async keeps every non-blocked endpoint answering in milliseconds.

The senior move

"When an entire web API stalls while CPU is sitting low, I immediately check ThreadPool queue length and thread counts for thread starvation, and I enforce async all the way down with Roslyn analyzers."

Sources Sourced: Microsoft guidance on ThreadPool starvation diagnostics, .NET runtime ThreadPool injection rate limits (1-2 threads/sec), Roslyn VSTHRD002 analyzer rules, and mechanics. Composed: UDC Finance dealer portal context, names, dates, and query parameters.

Traps

Traps

What should you say instead? Tap to check.

Live coding

Easy first, then harder. Set a timer and type it before you look.

Task 1

Reportedmedium · 15 minutes

Refactor a blocking controller and service chain to be completely asynchronous end to end, removing all .Result and .Wait() calls and supporting cancellation.

CS-code-async-end-to-end (Question Index)
You are given

The candidate is given this blocking sync-over-async code to refactor.

Lending.sln · where this file sits
  • src/Api/Controllers/
Stuck? Hints
  1. Change the controller action return type to Task<ActionResult<RateDto>> and add the async keyword.
  2. Replace .Result with await.
  3. Add a CancellationToken parameter to the action method and forward it to the service and database calls.
Show a solution
Lending.sln · where these 2 files sit
  • src/
    • Api/Controllers/
    • Application/
What the interviewer checks
  • Replaces .Result with await across the entire call chain
  • Returns Task<ActionResult<T>> instead of IActionResult
  • Accepts and passes CancellationToken down to HttpClient or EF Core
With AI tools

GitHub Copilot and Claude can quickly convert synchronous methods to async boilerplate and identify synchronous .Result calls across a codebase. However, AI frequently introduces subtle async anti-patterns: omitting , wrapping asynchronous I/O methods unnecessarily inside , or prematurely applying without measuring heap allocations. A senior engineer uses static analysis rules like in and verifies async state machine performance using and runtime counters.

Glossary: every name, one line each →

My notes

Paste the NOTES block from your doubt chat here. The grader never reads notes; the coach reads "Still unclear".

Read moreWhere it was asked