Skip to main content
CodeOath
← All posts

.NET Core / Web API71 min total · 19 parts

Building REST APIs with ASP.NET Core: Routing, Middleware, and Dependency Injection

Part 17 of 19 · ~2 min

Async/Await Throughout the Pipeline

Look back over every action, every EF Core call, every scrap of custom middleware in this reference and they're all async/await, consistently — and specifically inside a web API, that's not a taste preference, it's load-bearing. Kestrel has one shared, finite pool of threads to spread across every request it's currently juggling. Write a call that blocks — waiting on a query, waiting on some other service over HTTP — and the thread sitting under it is stuck doing precisely nothing until that wait is over, unavailable to touch any other request in the meantime. Write the same call as await, and the thread gets handed straight back to the pool the moment the wait starts, free to pick up other work, and whichever thread happens to be free when the operation finally finishes is the one that resumes it — not necessarily the one that started it.

Bench actually hit this, a full semester after the Tuesday-afternoon incident, and it's worth naming precisely because the symptom looks similar and the cause is completely different. Move-in week, everyone checking tool availability at once, and the kiosk screens started feeling frozen again. This was not the DI bug — that one had already been fixed for months. The actual culprit was a reporting endpoint Theo had written under deadline pressure:

// Ties up a pool thread until this whole aggregate query comes back
[HttpGet("/api/reports/weekly-usage")]
public IActionResult WeeklyUsage() =>
    Ok(_reservations.GetWeeklyUsageAsync().Result); // .Result — blocks, doesn't await

Reaching for .Result instead of await freezes the calling thread in place until the task finishes, and there's a nastier failure mode lurking behind it in some hosting setups — old-style ASP.NET's SynchronizationContext could get itself into a genuine deadlock this way, since the very thread stuck waiting was sometimes the one the task needed back to continue. That's rarer under Kestrel than it used to be under the old model, and it isn't quite what took down Bench's reporting page during move-in week — what actually happened was the plainer, far more common cost of blocking: a rush of members all opening the usage dashboard at once, each one pinning down a pool thread for as long as a genuinely slow aggregate query took to finish, and that pool is the identical one every reservation and availability request on Bench also has to draw from. Nothing about the kiosk stations was broken — they were simply stuck in line behind threads that should never have been sitting idle-but-blocked in the first place. Fixing it meant doing what this whole reference has quietly been demonstrating all along: swap .Result for await and keep swapping it at every level above that call, instead of letting one synchronous wait sit in the middle of what was supposed to be an async path start to finish.