Behavior of Synchronous IO in ASP.NET Core Under Concurrent Load
0 reputation · 30 Oct 2020, 03:16 UTC
0 reputation · 30 Oct 2020, 03:16 UTC
Determine how enabling synchronous IO in ASP.NET Core affects request latency when the server handles many concurrent connections.
The framework disables synchronous IO by default to prevent thread‑pool starvation. Legacy code may require it, but the documentation does not mandate a single configuration path.
It is unclear whether the default setting is optimal for all workloads, and what thresholds of concurrency trigger noticeable latency spikes.
The documented default policy in ASP.NET Core 7.0 is to disable synchronous IO; KestrelOptions.AllowSynchronousIO and IISOptions.AllowSynchronousIO are false, and any blocking read/write throws InvalidOperationException unless the switch is enabled.
On a typical 8‑core server the thread pool starts with hundreds of threads, but other ASP.NET work (request handling, middleware, timers) consumes a portion. Empirical observations note latency spikes around 200‑500 concurrent blocking requests, depending on I/O latency and hardware. Below that range the impact is usually negligible; above it the work queue grows and request latency rises noticeably.
var allowSync = app.Services.GetRequiredService>().Value.AllowSynchronousIO; (should be false).AllowSynchronousIO = true in Program.cs via builder.WebHost.ConfigureKestrel(options => options.AllowSynchronousIO = true);wrk -t4 -c200 -d30s http://localhost:5000/sync-endpoint) with the endpoint performing a blocking Thread.Sleep or FileStream.Read.dotnet-counters monitor --process-id System.Threading.ThreadPool-ThreadCount) and request latency.If you observe latency degradation at a concurrency level far below the 200‑500 range, please provide the average I/O latency (e.g., disk or network round‑trip time) of your blocking operation so the threshold can be refined.
Use comments to ask for clarification. Post a solution as an answer.
35,275 reputation · 30 Oct 2020, 12:57 UTC
When AllowSynchronousIO = true, Kestrel stops buffering request bodies larger than 30 KB. The entire payload must be held in memory during the synchronous read, increasing GC pressure and memory footprint under load. This behavior differs from the async path, which streams via PipeReader.
Instead of enabling globally, wrap only the legacy endpoints:
app.Use(async (ctx, next) => {
var syncFeature = ctx.Features.Get<IHttpRequestBodySizeFeature>();
if (syncFeature != null) syncFeature.MaxRequestBodySize = null; // or a limit
var kestrelFeature = ctx.Features.Get<IKestrelServerInformation>();
// No direct per-request AllowSynchronousIO; use a branch instead
await next();
});
A cleaner pattern is to map legacy routes to a separate MapWhen branch that sets AllowSynchronousIO = true on a cloned KestrelServerOptions instance, isolating the thread-pool impact.