← Home
.NETAsync and threadsReported by candidates

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

Asked at Deloitte, Infosys, TCS, IBM, Neudesic and Wipro tech rounds, and on AmbitionBox by many product firms.

Say this first · about 30 seconds
Make it yours · BMW FS lease buyout quotes (invented example)
The code, simplified

Invented to show the shape, in C#. Tap Next to follow one quote, and the token, down to FIS and back.

BMW.sln · where these 3 files sit
  • src/
    • Api/Controllers/
    • Application/Quotes/
    • Infrastructure/Fis/
Diagram · step 1A dealer opens a buyout quote. ASP.NET Core gives the action a ct tied to this request.
QuotesController.cs · line 17 · 1 of 3Receives the request's token. ASP.NET Core fills it in and cancels it if the dealer's browser disconnects.

Fix later in your repo Example written for this card: invented, not from a real codebase.

Say it out loud, then check

Show the 30-second answer
  • async/await lets a method wait for slow input and output, like a database call, without holding a while it waits.
  • async marks the method. It allows inside, and the method returns a : a result that comes later.
  • await: if the result isn't ready, the method pauses and its thread goes back to the thread pool, the app's shared set of threads, to serve other requests. When the result arrives, a pool thread carries on from the same line.
  • Inside, the compiler turns the method into a small state machine: an object that remembers which it reached and the values of its local variables.
  • Why in a web API: the same few threads serve many more requests at once, because no thread sits idle waiting on SQL or an HTTP call.
  • My rules: async all the way down, never .Result or .Wait(), and pass the through.
Check your answer: tick what you said

You said 0 of 9

Then add your example: BMW FS lease buyout quotes (invented example)
  • Situation: at BMW FS, the dealer portal shows what it costs to buy a leased car at the end. Our API asks FIS for the ; at month-end that takes 20 seconds.
  • What I found: every method was async, but never passed ct to the FIS client. So each refresh left the old FIS call running for nobody.
  • What I did: passed ct down to the HTTP call. Now, when the dealer's browser disconnects, ASP.NET Core cancels the token, our call is cancelled (FIS may still finish its work), and an (usually ) travels up through each and ends the request.
  • What I'd say: async frees the thread while we wait; the token stops work nobody waits for. Pass it through every layer.

See it

1/13ASP.NET Core keeps a pool of to run requests. Here, just two: T1 and T2.

In plain words

The senior answer · about 2 minutes
What it does
  • async and await let a method wait for I/O, like a SQL query or a call to another API, without keeping a thread busy while it waits.
  • When the code reaches and the Task isn't finished, the method saves where it is and returns. The thread goes back to the thread pool and serves someone else's request.
  • When the database answers, the rest of the method is put on the thread pool's queue of work, and whichever pool thread is free picks it up from the line after the .
  • The compiler does the bookkeeping. It turns the method into a state machine: the local variables become fields, and a state number says which to resume after.
Why it matters in a web API
  • ASP.NET Core serves every request from one shared thread pool. If each request holds its thread for a 300-millisecond SQL call, threads run out long before the CPU does.
  • When that happens, the pool adds new threads only slowly. After the first few, it adds about one or two a second, so a traffic spike becomes a queue and then timeouts. That is thread-pool starvation. You recognise it because requests are slow while the CPU is almost idle.
  • With await, the same threads serve far more requests at once. One request is not faster; the server just handles more of them, so we need fewer instances.
The rules I follow
  • Async all the way. Once one call is async, every caller up to the controller returns a Task and awaits it. I never block on a Task with .Result, .Wait() or .GetAwaiter().GetResult() in request code.
  • I pass the from the action down to every EF Core and call. If the customer closes the page, the query stops instead of running for nothing.
  • No , except UI event handlers. An method returns no Task, so its exception has no way back to the caller. Nobody can catch it, and it crashes the whole process.
  • Independent calls can run together with , but never two queries on the same , EF Core's object for one request's database work, because it can only run one query at a time.
In production
  • I find blocking calls by measuring, not guessing. dotnet-counters, Microsoft's command-line tool for live .NET numbers, shows the thread pool's thread count climbing slowly and its queue of waiting work growing. shows requests slow while the SQL and HTTP calls inside them stay fast.
  • In review I search for .Result and .Wait(), and we turn on the analyzer rule VSTHRD002, which flags blocking waits, so the build fails on the next one.

One request through every idea

, the Task, , the state machine, the thread going back to the pool, another thread carrying on, and the , in the order they happen for one GET /api/loans/{id}.

1/8 changes nothing yet: runs on T1 like any method, line by line.

The story

  1. Before

    Before async, a web API called the database the plain way: db.Loans.FirstOrDefault(...), the non-async twin of . The running the request sent the query, then sat and waited for the answer, doing nothing.

    1/8Before async: Request A runs on T1, and T1 waits while the database works.

  2. Why it had to change

    Under load every thread was stuck waiting on I/O (input and output: the database, another API, a file). New requests queued, and the server timed out with the CPU almost idle. That is thread-pool starvation.

  3. How it works now

    Put on the slow call. If the answer isn't there yet, the method pauses and hands its thread back. When the answer arrives, a pool thread carries on from the same line.

  4. What happens next

    The compiler writes the hard part for you: it turns the method into a small state machine that remembers which it reached and the values of its local variables. EF Core and give you methods ending in Async that return a .

  5. What we gained

    Far more requests per server with the same threads, fewer instances to pay for, and no timeouts at month-end peaks. One request is not faster; the server handles more of them at once.

In the lending API

The controller that serves GET /api/loans/{id}. The three marked lines are what makes it async.

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

Lending.sln · where this file sits
  • src/Api/Controllers/
⋯ 1 line in Deep⋯ 2 lines in Deep
LoansController.cs · line 9 · 1 of 3 lets this method use . It returns a , so ASP.NET Core knows the response comes later and can free the thread meanwhile.

The code, file by file

One GET request in the lending API, file by file: controller, service, repository. Every layer returns a Task and awaits the next one. Press Next line to follow Request A through the diagram.

Lending.sln · where these 3 files sit
  • src/
    • Api/Controllers/
    • Application/Servicing/Loans/
    • Infrastructure/Servicing/
Diagram · step 1ASP.NET Core keeps a pool of to run requests. Here, just two: T1 and T2.
LoansController.cs · line 6 · 1 of 7Who builds this: ASP.NET Core, once per request. The passes in (registered as in Program.cs).

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: Payoff quotes timed out at month-end

P2 · serious, has a workaroundBMW FSonly under loadLending API: payoff quotes

Composed, not a real event at BMW FS. Every mechanism is real; the pattern is Microsoft's own thread-pool starvation tutorial: Debug thread-pool starvation (Microsoft Learn)

The story in 5 lines
  1. On 1 October, BMW FS customers and dealers asking for a got spinning screens and timeouts. Every other page of the portal was slow too.
  2. showed 30-second requests but fast SQL and FIS calls, with CPU at 15%: the API was waiting for threads, not for work.
  3. was synchronous and blocked on the FIS client with .Result. Each quote held a pool thread for over a second; the month-end spike used up the pool.
  4. We scaled out to buy time, then shipped the chain async end to end with the passed through.
  5. The analyzer rule for blocking waits now fails the build, and a load test runs before every release.
Who's who (5 people)
You
.NET developer on the vendor team (L3). Finds the cause, writes the fix and the test.
Tech lead
Vendor senior. Runs the bridge call, reviews every change.
L2 pod
Vendor support in Navi Mumbai. First look, runbooks, scaling.
BMW FS IT manager
Approves changes, including the fast-track ones.
BMW FS digital team
Owns the customer portal that calls our API.

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

1/8 Detect

Month-end traffic hits the API. The response-time alert fires and pages .

Wed 1 Oct, 09:10 ET (18:40 IST)Azure Monitor alert, then L2

  • The 1st of the month: customers check amounts before their autopay, and dealers request on lease returns. Quote traffic is about ten times a normal day.
  • The alert "p95 response time over 5 s" fires on the lending API and pages the . BMW FS's digital team calls minutes later: screens spin and time out.
  • Not only : every endpoint on the same API is slow, even ones that never touch FIS.
The alert (Azure Monitor)
Sev 2 · lending-api-prod
Rule: requests/duration p95 > 5 s for 5 min
Fired: 2025-10-01 13:10 UTC
Current p95: 31.4 s · CPU: 15%
In theory

A load test before the feature went live would have shown the thread count climbing at month-end traffic.

In practice

The feature shipped in August and passed , where testers send one request at a time. Blocking code only fails under load.

Try it yourself · 20 minutes, on your Mac
  1. Clone Microsoft's diagnostic scenarios sample (linked from the Microsoft Learn starvation tutorial) and dotnet run it.
  2. Send load to the taskwait endpoint with a load tool such as bombardier, and watch dotnet-counters monitor in a second terminal.
  3. Do the same against taskasyncwait, the async version, and compare the thread count and the latency.
You should see

The blocking endpoint's thread count climbs slowly and latency jumps to seconds; the async endpoint stays near 500 ms with few threads.

The senior move

"When requests are slow but the CPU is idle and the database is fast, I look for threads that are waiting, not working. Then I make the chain async and make the build fail on the next blocking wait."

Sources Sourced: the starvation signals, the thread growth rate of one or two a second and the dotnet-counters names (Microsoft Learn starvation tutorial), sync-over-async guidance (David Fowler's async guidance), the analyzer rule VSTHRD002 (Microsoft.VisualStudio.Threading.Analyzers), and the scale-out command. Composed: BMW FS's systems, every date, number and name, and the (standard columns).

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

Question index: Musteasy · 5 minutes

This repository method blocks a thread while the database works. Make it async.

Warm-up for the end-to-end task (EF Core async docs)
You are given
Lending.sln · where this file sits
  • src/Infrastructure/Servicing/
PaymentRepository.cs · line 4 · 1 of 1Called by the payment-statement service. db is the request's , passed in by the container.
Stuck? Hints
  1. EF Core has an async version of . What is it called?
  2. What must the return type become?
  3. What should the caller be able to pass, so the query can stop?
Show a solution
Lending.sln · where this file sits
  • src/Infrastructure/Servicing/
PaymentRepository.cs · line 4 · 1 of 2Returns a Task now. The Async suffix tells callers to await it.
What the interviewer checks
  • instead of .
  • Returns Task<List<Payment>>, and the name ends in Async.
  • Takes a and passes it to .
  • Can say what the caller now has to do: it.

Task 2

Question index: Mustmedium · 20 minutes

Make this blocking controller → service → EF Core and HttpClient chain async end to end. Remove every .Result and .Wait().

Question index: CS-code-async-end-to-end (Must, medium); the same fix as Microsoft's thread-pool starvation tutorial
You are given
Lending.sln · where these 2 files sit
  • src/
    • Api/Controllers/
    • Application/Payoff/
PayoffController.cs · line 2 · 1 of 1 is passed in by the container.
Stuck? Hints
  1. Start at the bottom: which calls already return a Task?
  2. Each method that awaits must itself return a Task. Work upwards to the controller.
  3. Where does the controller get a , and who needs it?
  4. The solution passes ct into repo.GetAsync and fis.GetPayoffAsync, so add a CancellationToken ct parameter to both methods and their interfaces, and pass it to EF Core and inside.
Show a solution
Lending.sln · where these 2 files sit
  • src/
    • Api/Controllers/
    • Application/Payoff/
Diagram · step 5Request A arrives. Free thread T1 runs its code: the controller, then the service.
PayoffController.cs · line 1 · 1 of 5Routing runs this on a free pool thread, T1 in the diagram.
What the interviewer checks
  • No .Result, .Wait() or .GetAwaiter().GetResult() left.
  • Service returns and is named ; the interface changes too.
  • Controller action is async Task<ActionResult<PayoffQuote>> and awaits the service.
  • taken in the action and passed to EF Core and the FIS client; repo.GetAsync and fis.GetPayoffAsync get a ct parameter too.
  • The repository and FIS calls stay one after another: the second needs the first's .
  • Senior extra: say how you'd prove it worked: a load test before and after, and the thread count in staying flat.
With AI tools

Interviewers now ask how you check AI-written code. A story that lands: Copilot "fixed" a compile error, where an async client didn't fit a synchronous interface, by adding .GetAwaiter().GetResult(). It compiled and passed the tests, because tests run one request at a time. I rejected it and made the interface async instead, because blocking calls only fail under load. Now I ask the AI to review its own diff for blocking waits, and the analyzer rule catches what it misses.

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