📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

Which blocking calls cause problems in ASP.NET Core, how do you find them, and what should replace them?

Synchronous I/O and AllowSynchronousIO, EF Core sync APIs, HttpClient.Send, Thread.Sleep, long locks, heavy synchronous logging; detection with analysers, traces and load tests.

Mid-Level CodeReview Intermediate aspnet-core async blocking-calls performance

🎯 Key points

  • Blocking calls hold request threads while waiting and cause starvation
  • Examples: .Result/.Wait, sync body I/O, EF Core sync methods, HttpClient.Send, Thread.Sleep, locks around I/O
  • Kestrel disallows synchronous body I/O by default (AllowSynchronousIO = false)
  • Replace with ReadAsync, ToListAsync, SaveChangesAsync, SendAsync, Task.Delay, SemaphoreSlim.WaitAsync
  • Use async middleware and filters for I/O
  • Find blocking with CA1849, VSTHRD analysers, stack snapshots and load tests

⏱ 30-second answer

Any call that holds a request thread while waiting is a blocking call: sync-over-async (.Result, .Wait()), synchronous I/O (Stream.Read on the request body, File.ReadAllText, EF Core ToList()/SaveChanges(), HttpClient.Send), Thread.Sleep, long lock sections and synchronous logging to slow sinks. Kestrel disallows synchronous request and response body I/O by default (AllowSynchronousIO = false) and throws InvalidOperationException if you try. Replace them with async equivalents (ReadAsync, ToListAsync, SaveChangesAsync, SendAsync, Task.Delay, SemaphoreSlim.WaitAsync), find them with analysers (CA1849), stack snapshots and load tests.

🗣 Full answer (senior)

How I would answer this in an interview

"In ASP.NET Core a blocking call is anything that holds a request thread while it waits. The usual suspects are sync-over-async (.Result, .Wait()), synchronous I/O (reading the request body with ReadToEnd, Request.Form, File.ReadAllText, EF Core's ToList()/SaveChanges(), HttpClient.Send), Thread.Sleep in retry loops, locks around I/O, and synchronous logging to slow sinks. Each one removes a pool thread for its whole duration. Under load that becomes ThreadPool starvation: higher latency and lower throughput for every endpoint, not just the slow one. Kestrel helps: since 3.0 it disallows synchronous body I/O by default (AllowSynchronousIO = false) and throws instead, and turning that back on just hides the problem. Replacements are the async equivalents: ReadAsync, ReadFormAsync, ToListAsync, SaveChangesAsync, SendAsync, await Task.Delay, SemaphoreSlim.WaitAsync, async/batched logging. To find blocking I use analysers (CA1849, VSTHRD), stack snapshots under load, and load tests that watch pool queue length."


1. Why it matters

200 concurrent uploads from slow mobile clients
sync ReadToEnd holds a thread ~5 s each  -> 200 blocked pool threads
pool minimum = cores (e.g. 4) -> slow injection -> queue grows
every other endpoint waits behind it -> p99 for /health, /login, /search spikes

2. Blocking calls and replacements

Blocking Async replacement
task.Result, task.Wait(), GetAwaiter().GetResult() await task
Request.Body.Read, StreamReader.ReadToEnd() ReadAsync, ReadToEndAsync, ReadFromJsonAsync, model binding
Request.Form (sync property) await Request.ReadFormAsync(ct) / [FromForm] binding
IFormFile.CopyTo CopyToAsync
Response.Body.Write, Response.WriteAsync(...).Wait() await Response.WriteAsync(...)
EF Core ToList, First, Count, SaveChanges ToListAsync, FirstAsync, CountAsync, SaveChangesAsync
HttpClient.Send SendAsync, GetFromJsonAsync
File.ReadAllText, File.WriteAllText ReadAllTextAsync (or cache at startup)
Thread.Sleep await Task.Delay / resilience pipeline
lock around I/O SemaphoreSlim.WaitAsync, or remove shared state
Process.WaitForExit() WaitForExitAsync
next(context).Wait() in middleware await next(context)
Sync filters doing I/O (IActionFilter) IAsyncActionFilter
Sync logging to network sinks Async/batched sinks, background-queued providers

3. Kestrel's guard

Setting Default Behaviour
KestrelServerOptions.AllowSynchronousIO false (since ASP.NET Core 3.0) Sync body reads/writes throw InvalidOperationException: Synchronous operations are disallowed
IIS in-process IISServerOptions.AllowSynchronousIO false Same

If a legacy library requires sync I/O, allow it per request via IHttpBodyControlFeature.AllowSynchronousIO for that endpoint only, and track it as a defect, rather than enabling it globally.

4. Finding blocking

Technique Catches
CA1849, VSTHRD002, VSTHRD103 Sync calls where async exists; sync waits
dotnet-stack / dumps under load Library-internal blocking (e.g. SDKs, logging sinks)
Load tests (k6, bombardier) + dotnet-counters Queue length growth, thread creep
Slow-client tests Sync body reads that only hurt on slow networks
Code review checklist Thread.Sleep, lock + I/O, AllowSynchronousIO = true

Production example

Our [mobile backend]'s photo-upload endpoint read the body with new StreamReader(Request.Body).ReadToEnd(), and years earlier someone had set AllowSynchronousIO = true globally to silence the exception. Whenever many users uploaded on slow mobile networks, all endpoints' latency spiked while CPU stayed low. dotnet-stack snapshots showed many threads blocked in HttpRequestStream.Read from that endpoint, each held for seconds by slow clients. I switched to ReadToEndAsync, then went further and streamed the body directly to blob storage with CopyToAsync (no full buffering), and removed the global AllowSynchronousIO. The spikes disappeared; we enabled CA1849 as an error and added a slow-client load test (throttled bandwidth) to the release pipeline.

🔍 What the interviewer is checking

Checks practical knowledge of web server threading.

Green flags

  • Lists common blocking calls and their async replacements.
  • Knows AllowSynchronousIO and why it defaults to false.
  • Describes analysers and load testing to find blocking.

Red flags

  • Thinks blocking is fine because "the pool will add threads".
  • Turns on AllowSynchronousIO as a fix.

📚 Learn in depth

1. Waiting vs computing

Work on a request thread Verdict
Short CPU (validation, mapping, small serialisation) Fine
Waiting for I/O synchronously Bad: blocks a thread for nothing
Long CPU (seconds: PDFs, image processing, big reports) Move to a bounded background queue / separate service

The distinction matters because Task.Run doesn't fix waiting (see async-over-sync), and long CPU work needs capacity planning, not async.

2. Middleware and filters

// WRONG
app.Use((ctx, next) => { Audit(ctx); next(ctx).Wait(); return Task.CompletedTask; });

// RIGHT
app.Use(async (ctx, next) =>
{
    await AuditAsync(ctx, ctx.RequestAborted);
    await next(ctx);
});

Synchronous filter interfaces are fine for CPU logic; for anything that touches the database, cache or HTTP, implement IAsyncActionFilter / IAsyncAuthorizationFilter.

3. Logging

  • Built-in providers (console, debug) queue messages on a background thread, but when the queue is full, logging calls block until there's space. Under very high log volume, that turns logging into a blocking call.
  • Third-party sinks (Serilog, NLog) may write synchronously unless configured with async/batching wrappers.
  • Avoid logging full request/response bodies on hot paths; use sampling and levels.

4. Database drivers and "fake async"

Modern providers (Microsoft.Data.SqlClient, Npgsql, MySqlConnector) implement real async I/O. Some older providers implement ...Async methods by running the sync version, which still blocks. The method name proves nothing; check docs and look at stacks under load.

5. Streaming instead of buffering

app.MapPost("/photos", async (HttpRequest req, BlobContainerClient blobs, CancellationToken ct) =>
{
    var blob = blobs.GetBlobClient($"{Guid.NewGuid()}.jpg");
    await blob.UploadAsync(req.Body, overwrite: false, ct);   // streams; no full in-memory copy
    return Results.Created(blob.Uri, null);
});

Streaming with async I/O fixes both blocking and memory spikes for large payloads (configure MaxRequestBodySize deliberately).

6. Version notes

Version Change
ASP.NET Core 3.0 AllowSynchronousIO defaults to false (Kestrel, IIS, TestServer)
.NET 5+ ReadFromJsonAsync / WriteAsJsonAsync helpers
.NET 7 ReadToEndAsync(CancellationToken) overload
.NET 8+ Analyser improvements (CA1849 broader coverage)
.NET 10 Same guidance

7. Try it yourself

  1. Call /upload-bad from the code example and observe InvalidOperationException.
  2. Enable AllowSynchronousIO, load-test with a throttled client (e.g. curl --limit-rate 10k in parallel), and watch pool metrics; then switch to /upload.
  3. Add Thread.Sleep(200) in an endpoint, load-test, then replace with await Task.Delay(200).
  4. Write a middleware that does next(ctx).Wait() and observe its effect under load.
  5. Enable CA1849 in .editorconfig (dotnet_diagnostic.CA1849.severity = warning) and fix findings.
  6. Configure a logging sink without batching, log heavily under load, and capture stacks to see logging frames.

💻 Code

using System.Text.Json;

var builder = WebApplication.CreateBuilder(args);
// Do NOT do this to silence errors:
// builder.WebHost.ConfigureKestrel(o => o.AllowSynchronousIO = true);
var app = builder.Build();

// BAD (throws by default; blocks if AllowSynchronousIO is enabled)
app.MapPost("/upload-bad", (HttpRequest request) =>
{
    using var reader = new StreamReader(request.Body);
    string text = reader.ReadToEnd();
    return text.Length;
});

// GOOD: async body reading, async delay, async lock
var gate = new SemaphoreSlim(1, 1);
app.MapPost("/upload", async (HttpRequest request, CancellationToken ct) =>
{
    using var reader = new StreamReader(request.Body);
    string text = await reader.ReadToEndAsync(ct);

    await gate.WaitAsync(ct);
    try
    {
        await Task.Delay(10, ct); // stands in for async I/O under a shared limit
    }
    finally
    {
        gate.Release();
    }
    return Results.Ok(new { Length = text.Length });
});

// GOOD: model binding / ReadFromJsonAsync are already async
app.MapPost("/orders", async (HttpRequest request, CancellationToken ct) =>
{
    var order = await request.ReadFromJsonAsync<JsonElement>(ct);
    return Results.Accepted(value: order.GetProperty("id").GetInt32());
});

app.Run();

⚠ Common mistakes

1. Enabling AllowSynchronousIO to silence an exception

What happens: o.AllowSynchronousIO = true globally. Why it is wrong: re-enables blocking body I/O that starves the pool under load. Do instead: async reads/writes; per-request opt-in only for a specific legacy dependency.

2. Mixing EF Core sync and async methods

What happens: ToListAsync in one place, SaveChanges() in another. Why it is wrong: sync calls block threads on database I/O. Do instead: async variants consistently in request code.

3. Thread.Sleep for retries/throttling

What happens: retry loops sleep in endpoints. Why it is wrong: blocks the thread for the whole delay. Do instead: await Task.Delay or a resilience pipeline.

4. Assuming ...Async means non-blocking

What happens: a library's async method is trusted blindly. Why it is wrong: some implementations block internally. Do instead: check docs and stacks under load.

5. Reading Request.Form synchronously

What happens: var name = Request.Form["name"]; in an endpoint. Why it is wrong: forces a synchronous read of the body. Do instead: await Request.ReadFormAsync(ct) or [FromForm] binding.

6. Synchronous middleware calling next(...).Wait()

What happens: custom middleware written synchronously. Why it is wrong: blocks a thread for the entire downstream pipeline. Do instead: await next(context).

7. Doing I/O in synchronous filters

What happens: IActionFilter.OnActionExecuting queries a cache synchronously. Why it is wrong: blocking on every request. Do instead: IAsyncActionFilter.

8. Locks around I/O in shared services

What happens: lock (_gate) { _cache = LoadFromDb(); }. Why it is wrong: blocks callers and holds a lock during I/O. Do instead: SemaphoreSlim.WaitAsync and async I/O, or refresh in the background.

9. Synchronous logging to network sinks

What happens: every log line is sent synchronously to a remote endpoint. Why it is wrong: logging latency becomes request latency; blocks threads. Do instead: batched async sinks, reasonable log levels.

10. Buffering large uploads entirely in memory

What happens: ReadToEndAsync on multi-MB bodies. Why it is wrong: not blocking, but memory spikes and GC pressure. Do instead: stream to storage with CopyToAsync / SDK streaming uploads.

🏛 Senior insight: trade-offs

In ASP.NET Core the rule is simple: never wait on a request thread. I enforce it with analysers, Kestrel's default sync I/O guard and load tests, and I treat any AllowSynchronousIO = true as a tracked defect.

Control Purpose
CA1849, VSTHRD analysers Catch blocking calls at build time
Kestrel AllowSynchronousIO = false Fail fast on sync body I/O
Load tests with pool metrics Catch hidden blocking in libraries
Stack snapshots in incidents Find the exact blocking frame

🔗 Follow-ups the interviewer may ask

  1. When: Candidate says blocking starves the pool Why: Checks starvation diagnosis
    How to answer

    ThreadPool starvation happens when pool threads are blocked faster than the pool can add new ones, so queued work (requests, continuations, timers) waits. Symptoms: latency rises sharply while CPU stays low, the pool queue length grows, thread count climbs slowly, health checks time out and Kestrel logs heartbeat warnings mentioning possible starvation. I confirm it with dotnet-counters (queue length and thread count), then take dotnet-stack snapshots or a dump to see what the threads are blocked on, usually .Result, .Wait(), synchronous I/O or locks. The fix is to make that code asynchronous; raising SetMinThreads only buys time.

  2. When: Candidate lists .Result and .Wait Why: Checks sync-over-async
    How to answer

    Sync-over-async is synchronously blocking on asynchronous work: .Result, .Wait(), GetAwaiter().GetResult() or Task.WaitAll. It holds a thread for the whole operation, causes ThreadPool starvation on servers and deadlocks under a SynchronizationContext. It sneaks in through constructors, DI factory lambdas, property getters, Dispose, lock blocks, synchronous interfaces (IComparer, old filters), static initialisers and "quick fixes" in legacy code. Remove it by making the call chain async, using async factory methods or lazy async initialisation, IAsyncDisposable, hosted-service warm-up, and async-capable interfaces. Where a sync boundary is unavoidable (for example a legacy interface), keep it at the edge and document it.

  3. When: Candidate replaces lock with an async gate Why: Checks SemaphoreSlim
    How to answer

    SemaphoreSlim is a lightweight, in-process counting semaphore that supports WaitAsync, so it works with async/await. With a count of 1 it is an async lock: await gate.WaitAsync(ct); try { ... } finally { gate.Release(); }. With a count of N it limits how many async operations run at once, for example ten concurrent calls to a partner API. It is not re-entrant and has no owner, so calling it again from inside the protected section deadlocks, and every successful wait must be paired with exactly one Release in finally.

🛠 Real-world problems: how you handle them

Upload endpoint blocking on slow mobile clients
Situation
A [mobile backend] upload endpoint used `ReadToEnd()` with `AllowSynchronousIO = true`, set years earlier.
Signals
Latency spikes across all endpoints when many users uploaded photos on slow networks; low CPU.
Investigation
Stack snapshots showed many threads blocked in `HttpRequestStream.Read` from the upload endpoint.
Resolution
Switched to `ReadToEndAsync` and later streamed the body directly to blob storage; removed `AllowSynchronousIO`.
Prevention
Enabled CA1849 as an error and added a slow-client load test.

How to tell it (STAR)

Our [mobile API] slowed down whenever users uploaded photos on bad connections. The stacks showed threads blocked reading request bodies synchronously, because someone had enabled sync I/O long ago. I made the reads async, then streamed uploads straight to storage, and removed the setting. The spikes went away.

↩ Usually asked after

More in Concurrency Production Problems

Last updated 31 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me