.NETAsynchronous programmingReported by candidates
How does async/await work, and why use it in a web API?
Reported across Deloitte, IBM, Infosys, TCS and other .NET interviews. `ConfigureAwait(false)` asks an await not to resume through a captured context; interviewers also ask how await pauses a method.
Say this first · about 30 seconds
An API request often waits on a database or another service. That is I/O-bound work: the app waits for another system. With synchronous code, the request's thread sits blocked during that wait.
With and , the method returns a that represents work finishing later. When an awaited I/O call is still pending, the method saves what it needs and returns; the can serve another request.
When the I/O finishes, the continuation (the saved remainder of the method) is scheduled and execution resumes, possibly on a different thread. The task for this operation completes with this request's result, which flows back to the controller.
That lets the API handle more concurrent (in-flight at once) I/O-bound requests (waiting on another system) with the same thread pool. It can improve throughput (completed requests per unit of time); it does not make the database query faster, create a thread per request, or make CPU-heavy (processor-work) code faster.
Make it yours · UDC Finance disclosure API (invented example)
Situation: during UDC's wider rollout, the API generated disclosure PDFs while dealers waited for the response.
Problem: an older helper called RenderAsync(...).Result. A burst of requests blocked thread-pool threads; even health checks queued while the document service was slow.
What I changed: I used through the controller and service calls. We later moved bulk PDF generation to a background , because a request should not keep dealers waiting for a long document job.
Result: at twice the rollout peak, p95 stayed under a second. I added a load test and a build rule against .Result / .Wait() in API request code.
What I'd say: async improved how many waiting requests the API could serve; moving long work out of the request reduced user wait time. They solve different parts of the problem.
The code, simplified
Invented .NET 8 API code. A dealer posts application details; ASP.NET binds them into LoanData. Dependency injection supplies LoanService, its typed DocumentClient, and the document API wrapper over HttpClient. ct comes from RequestAborted; the remote API returns PDF bytes.
UDC.sln · where these 3 files sit
src/Api/
Controllers/
Services/
Clients/
Diagram · step 1The dealer posts details; ASP.NET passes the model and cancellation token to the controller.
Dealerposts details
API request
Controllerawaits service
DisclosureController.cs · line 1 · 1 of 5ASP.NET calls this action; IActionResult represents its HTTP response.
Fix later in your repoInvented example based on the UDC thread-pool-starvation hook; composed, not a claim about a real event.
Say it out loud, then check
Show the 30-second answer
An API request often waits on a database or another service. That is I/O-bound work: the app waits for another system. With synchronous code, the request's thread sits blocked during that wait.
With and , the method returns a that represents work finishing later. When an awaited I/O call is still pending, the method saves what it needs and returns; the can serve another request.
When the I/O finishes, the continuation (the saved remainder of the method) is scheduled and execution resumes, possibly on a different thread. The task for this operation completes with this request's result, which flows back to the controller.
That lets the API handle more concurrent (in-flight at once) I/O-bound requests (waiting on another system) with the same thread pool. It can improve throughput (completed requests per unit of time); it does not make the database query faster, create a thread per request, or make CPU-heavy (processor-work) code faster.
Check your answer: tick what you said
You said 0 of 6
Then add your example: UDC Finance disclosure API (invented example)
Situation: during UDC's wider rollout, the API generated disclosure PDFs while dealers waited for the response.
Problem: an older helper called RenderAsync(...).Result. A burst of requests blocked thread-pool threads; even health checks queued while the document service was slow.
What I changed: I used through the controller and service calls. We later moved bulk PDF generation to a background , because a request should not keep dealers waiting for a long document job.
Result: at twice the rollout peak, p95 stayed under a second. I added a load test and a build rule against .Result / .Wait() in API request code.
What I'd say: async improved how many waiting requests the API could serve; moving long work out of the request reduced user wait time. They solve different parts of the problem.
See it
API requestcontroller → service
Thread poolshared worker threads
SQL databaseI/O takes time
GetLoanAsync(42)
1/6A request reaches the controller, which starts the service call for loan 42.The service starts an asynchronous SQL query and reaches .SQL is pending. The method saves its place and returns; the thread can serve another request.The thread pool runs another request while SQL is still working.SQL returns the row. The task completes with the loan result.The continuation is scheduled. The method resumes, often elsewhere, and returns the response.
In plain words
A web request asks SQL for a loan. SQL takes 200 milliseconds to answer. With a blocking call, the API's thread waits doing nothing for all 200 milliseconds.
With an asynchronous database call, lets the method pause without keeping that thread. The API can use it for a different request while SQL works.
When SQL replies, the saved method continues, possibly on another thread, and sends the loan back. The task for this operation completes with this request's loan.
This helps when many requests wait on I/O at once. It doesn't speed up SQL, and it doesn't help CPU-heavy work by itself.
The senior answer · about 2 minutes
What the keywords do
I explain as compiler support, not a command to start a thread. The compiler turns the method into a state machine (generated code that remembers where to resume) which returns a or .
At each , if the operation is incomplete, the method stores its continuation (the remaining work) and returns control. If it has already completed, the method continues without suspending.
The task carries completion, a result, cancellation or an exception back to the caller. I await it instead of reading .Result or calling .Wait().
Why an API uses it
A request spends time waiting on EF Core (the .NET data-access library), (the .NET HTTP client), or a file. Asynchronous I/O does not keep a thread occupied just to wait, so the thread pool can run other requests.
This improves throughput when many requests are waiting at once. It does not make one slow dependency faster; I still measure latency and fix the dependency if it is the bottleneck.
I keep the whole call chain asynchronous: controller, service, and EF Core or API. A synchronous wait in the middle blocks a thread and can cause thread-pool starvation under load.
What I watch in production
I pass through to database and HTTP calls that accept a cancellation token, so work can stop after the client disconnects. Cancellation is cooperative; every callee must observe the token, and a database provider may not honor it immediately.
I don't use to wrap asynchronous I/O. For genuinely CPU-heavy work, I measure the cost and consider a bounded background if it does not need to finish inside the request.
I don't run parallel EF Core operations on the same ; async does not make that object safe for concurrent use. See .
One request, three layers
Follow loan 42 from the HTTP request to SQL and back. Every layer returns its task to the caller.
HTTP requestcontroller action
Loan serviceawaits EF Core
SQL databaseI/O in progress
1/3The request passes its token into the service call.The service awaits EF Core; SQL keeps working while the method yields.SQL returns a row; the service resumes and the controller answers 200 or 404.
The story
Before
Before: a .NET API called SQL synchronously. While SQL was working, the request thread stayed blocked. At low traffic this looked fine.
Request A
API threadblocked
SQLquery running
No thread freerequest B queues
1/1The request thread waits in place while SQL runs.
Why it had to change
Problem: at the rollout peak, a burst of slow document calls left too many threads blocked. Even health checks queued while CPU stayed low.
Many requests
Thread poolthreads blocked
Slow dependencyI/O pending
1/1Blocked threads pile up while the dependency is still working.
What happens next
Fix: make the I/O call asynchronous end to end. When it is pending, the thread returns to the pool instead of waiting on .Result.
Request Aawait SQL
Thread poolthread released
SQLquery continues
1/1The method pauses; SQL continues, and the thread can serve another request.
How it works now
How it works: the compiler saves the method's continuation at ; when the task completes, execution resumes and the result returns to the caller.
SQLloan row
Taskresult + continuation
Controllerbuilds response
1/1The completed task carries the result back, then the saved method continues.
In the lending API
ASP.NET calls the controller. Dependency injection supplies its service and scoped database context. The awaited EF Core query returns a loan or null; the controller answers 200 or 404.
Only the key lines are shown; Deep walks the whole file.
Lending.sln · where this file sits
src/Api/Controllers/
⋯ 1 line in Deep
LoansController.cs · line 9 · 1 of 2Passes the route id and client-disconnect token to the service, then awaits its result.
The code, file by file
ASP.NET Core invokes the controller with the route id and RequestAborted token (cancelled if the client disconnects). Dependency injection supplies LoanService and its request-scoped LoansDbContext (EF Core's object for querying the database and tracking entities). EF Core is the .NET library that turns C# queries into SQL returns the database Loan or null; the controller sends 200 or 404.
Lending.sln · where these 2 files sit
src/Api/
Controllers/
Services/
LoansController.cs · line 1 · 1 of 7This field holds the service the controller receives.
What they ask next
The follow-ups interviews really ask. Answer out loud, then tap to check.
ReportedWhat does ConfigureAwait(false) do?
ConfigureAwait(false) tells an await not to capture the current SynchronizationContext (a place the app expects the continuation to return to) or a non-default TaskScheduler. ASP.NET Core has no request SynchronizationContext and normally uses the default scheduler, so it usually changes nothing there. UI apps and classic ASP.NET can have a context to return to. explains continuations.
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
ReportedWhat does ConfigureAwait(false) do?
ConfigureAwait(false) tells an await not to capture the current SynchronizationContext (a place the app expects the continuation to return to) or a non-default TaskScheduler. ASP.NET Core has no request SynchronizationContext and normally uses the default scheduler, so it usually changes nothing there. UI apps and classic ASP.NET can have a context to return to. explains continuations.
Does pause execution?
It pauses this async method only when the task is incomplete. It does not pause the thread: the method saves its continuation and returns to its caller.
What happens if the awaited task throws or is canceled?
The exception is rethrown at the point, so normal try/catch around the await handles it. Cancellation is cooperative and is observed only if the operation supports the token.
How is I/O-bound work different from CPU-bound work?
I/O-bound work waits on another system, so async frees the thread during the wait. CPU-bound work must execute on a CPU; may move it to a pool thread, but does not reduce the CPU work.
Production bugs, and how to tell them
Each one: what you see, why, the fix in code, and the line to say.
Blocking on an async operation
Request queue rises while CPU stays low.
Cause
.Result blocks a request thread until the incomplete task finishes. A burst of slow I/O can leave too few pool threads available for other requests.
Fix
Await through the controller, service and repository. The main sequence shows why the thread is released; the incident shows the production fix.
Before · DocumentHelper.cs · line 1 · 1 of 1Blocks this request thread until the HTTP operation completes.
Say it
The helper blocked request threads on async I/O. We awaited the whole call chain and load-tested concurrent requests.
Production incident: The whole API stalled during the UDC rollout
P1 · outage or data leakUDC Financeonly under loadDisclosure API and document service
Composed, not a real event at UDC Finance. Reuses the async/thread-pool hook from incident #28; the dates, numbers and names below are invented.
The story in 5 lines
During a UDC rollout peak, every API endpoint, including health checks, took 10–20 seconds. The incident was , the highest-priority severity in this story.
showed low CPU but rising thread count and queued requests.
A shared PDF helper called an async document API with .Result, holding a thread for each request.
We disabled on-request PDFs, changed the call chain to async and moved bulk generation to a queue.
A 2× load test now holds p95 below one second; a build rule prevents .Result and .Wait() in API request code.
Who's who (4 people)
You
.NET developer on the vendor team; the disclosure flow was yours.
Sushmita
Backend lead; led triage and approved the mitigation.
Nitin
DevOps; scaled the App Service and gathered counters.
UDC incident manager
Chaired the bridge and coordinated dealer updates.
Tap a step. The map lights the place where it happens.
Dealer portalbulk PDF requests
Disclosure APIApp Service
PDF helpershared package
Document serviceslow HTTP I/O
Thread poolblocked by .Result
Health checksqueued too
You + backend leadvendor team
Azure DevOpsfix + load test
1/8 Detect
Dealers report a frozen portal; shows p95 at 14 seconds, even for health checks.
Tue 28 Apr 2026, 10:30 NZST = 04:00 ISTUDC dealers; you on call
Dealers report the portal freezing during the rollout (a release reaching users). , Microsoft's request-telemetry service, shows p95 (the time 95% of requests finish within) rising from about 700 ms to 14 seconds; health checks (automated probes that test whether the API responds) fail too.
The alert pages you at 04:05 IST. The incident manager opens the and you bring in Sushmita.
The issue spans endpoints, not only disclosures, during a burst of roughly 40 bulk document requests. QA reproduces it in (the system-integration test environment).
App Insights: request p95
requests
| where timestamp > ago(30m)
| summarize p95=percentile(duration,95)
by bin(timestamp,1m), name
| render timechart
In theory
Low CPU with rising thread count and queued requests points to blocked , not a CPU bottleneck.
In practice
In this composed incident, the first symptom was low CPU with a growing request queue.
AI assist · GitHub Copilot Chat, if UDC policy allows it
Drafted an chart query in Query Language () from the request-p95 question.
You checked:The table, duration column and time window against the schema and known deployment time.
Try it yourself · 15 minutes, locally, no cloud
Create a small .NET 8 console project and run the proof program in app/qa/writer-test2/proof-luna.
Run it once with a deliberately blocking wait, then with ; compare the marker order and thread IDs.
For the API shape, replace the blocking helper in a local controller/service/repository example with async methods and pass the cancellation token.
You should see
Both versions return the same value. The awaited version does not occupy the caller thread while the fake I/O delay is pending.
The senior move
When the UDC disclosure flow was under load, our shared helper blocked request threads on .Result while a document service was slow. I helped reproduce it with a concurrent load in and .NET runtime counters, then made the request chain async and moved bulk generation to a queue. At twice peak, p95 stayed below a second. Async freed threads during I/O; the queue kept long document work out of the dealer's request.
SourcesSourced hook: docs/work-story/06-incidents/00-index.md #28 and the UDC incident file. Composed: this card's incident dates, names, metrics and code. Runtime behavior for async I/O and thread-pool use is checked in the proof program and Microsoft Learn references on the card.
Traps
Don't say starts a new thread.Say marks a method for compiler transformation. Awaiting pending I/O releases the current thread; no thread is needed while the device or server works.
Don't sayAsync makes SQL faster.SayAsync can improve API throughput when requests wait on I/O. SQL latency is still the same unless the query or database changes.
Don't sayWrap every database call in .SayEF Core already has asynchronous I/O APIs. would occupy a thread to wait and add scheduling overhead.
Don't say always switches threads.SayIf the task is already complete, execution continues inline. Otherwise the continuation is scheduled later, and it may run on another thread.
Traps
What should you say instead? Tap to check.
Don't say starts a new thread.
Say instead
marks a method for compiler transformation. Awaiting pending I/O releases the current thread; no thread is needed while the device or server works.
Don't sayAsync makes SQL faster.
Say instead
Async can improve API throughput when requests wait on I/O. SQL latency is still the same unless the query or database changes.
Don't sayWrap every database call in .
Say instead
EF Core already has asynchronous I/O APIs. would occupy a thread to wait and add scheduling overhead.
Don't say always switches threads.
Say instead
If the task is already complete, execution continues inline. Otherwise the continuation is scheduled later, and it may run on another thread.
Live coding
Easy first, then harder. Set a timer and type it before you look.
Task 1
Question index: Mustmedium · 20 minutes
Medium · Make the blocking controller → service → EF Core call chain asynchronous end to end; remove .Result / .Wait() and pass the request cancellation token.
Broken three-file request path. ASP.NET invokes the controller; dependency injection supplies service, repository and scoped DbContext. filters by id; copies row values into LoanDto; SingleOrDefaultAsync runs the database query and returns one row or null.
Lending.sln · where these 3 files sit
Controllers/
Services/
Data/
Stuck? Hints
Start with the EF Core call and return Task<Loan?> from the repository.
Make the service return Task<LoanDto?> and await the repository.
Make the controller return Task<ActionResult<LoanDto>> and await the service; pass RequestAborted as ct.
Show a solution
Lending.sln · where these 3 files sit
Controllers/
Services/
Data/
Diagram · step 1A request reaches the controller, which starts the service call for loan 42.
API requestcontroller → service
Thread poolshared worker threads
SQL databaseI/O takes time
GetLoanAsync(42)
LoansController.cs · line 1 · 1 of 2The action returns a task for the eventual HTTP response.
What the interviewer checks
No .Result,.Wait() or GetAwaiter().GetResult() remains in the request path.
The controller → service → repository signatures all return Task / Task<T>.
The database call uses SingleOrDefaultAsync(ct), and the same token reaches it.
Loan not found still returns 404; a row still returns 200.
Do not add Task.Run around EF Core I/O.
With AI tools
AI coding tools often suggest .Result as the smallest patch or wrap EF Core in . Review the whole request path, verify every signature and cancellation token, and run a load test; the compiler alone will not reveal thread-pool starvation.