FrameworksPart 1
The Middleware That Looked Expensive Was Not the One Spending the Bytes
Removing UseRouting dropped per-request allocation by 2.29 KB in isolation — separated from authorization's share, routing itself costs about 24 bytes.

In short
- Removing UseRouting from a fixed six-component middleware pipeline dropped per-request allocation by 2.29 KB, the largest single drop of the six — but isolating routing's own cost from what disappeared alongside it puts routing at about 24 bytes.
- The missing 2.27 KB belongs to UseAuthorization, which only evaluates a policy when routing has already selected an endpoint carrying RequireAuthorization metadata; remove routing and authorization's own cost vanishes with it.
- A combined variant that removes both UseRouting and UseAuthorization at once allocates within a byte of NoRouting alone, which is what separates the two costs and shows the leave-one-out deltas are not additive.
- Corrected for the overlap, the ranking by per-request allocation is UseAuthorization (roughly 2.3 KB) ahead of UseAuthentication (roughly 1.27 KB), UseHsts (roughly 0.42 KB), UseRouting (roughly 24 bytes), and UseHttpsRedirection/UseExceptionHandler (effectively zero) — largely confirming what the middleware names already suggest.
- Wall-clock timing on this harness flipped direction across three runs and is reported as noise, not evidence; Allocated held to within 0.01 KB across the same three runs, including a run on an idle host.
Removing UseRouting from a fixed six-component ASP.NET Core pipeline dropped per-request allocation by 2.29 KB — the largest drop of any of the six components tested, ahead of both authentication and authorization. Isolated from what disappeared alongside it, routing’s own share of that drop is about 24 bytes.
The remaining 2.27 KB belongs to UseAuthorization. It only shows up as routing’s cost because removing UseRouting also removes the one thing authorization needs before it does any work: an endpoint carrying RequireAuthorization metadata.
A seventh benchmark variant, built to remove both middleware at once, is what separates the two costs — and it changes which of the six pipeline components measured here looks expensive without changing which one actually is.
The pipeline, one component at a time
The method is the same one-switch-at-a-time approach used to isolate Server GC’s effect on collection counts: hold everything fixed, change exactly one thing, measure. Here the fixed thing is a six-component pipeline built in the order ASP.NET Core’s documentation recommends — UseExceptionHandler(), UseHsts(), UseHttpsRedirection(), UseRouting(), UseAuthentication(), UseAuthorization() — in front of a single GET /ping endpoint that always returns 200.
The host is a generic HostBuilder with a classic IApplicationBuilder.Configure() pipeline, not WebApplication. That choice is load-bearing: WebApplication re-adds UseRouting, UseAuthentication and UseAuthorization automatically whenever the matching services are registered in DI, regardless of whether the call appears in code — deleting app.UseAuthorization() from a WebApplication pipeline that still calls AddAuthorization() would not remove the middleware at all. The classic host carries no such behaviour, so deleting a line here removes the middleware it names.
Six leave-one-out variants each drop exactly one component and re-measure the same GET /ping request through Microsoft.AspNetCore.TestHost.TestServer, with [MemoryDiagnoser] attached and no Stopwatch loop in the harness. Removing UseRouting also removes its dependent UseEndpoints/MapGet registration; that variant’s pipeline ends in a terminal app.Run(...) writing the same 200 response, so what disappears is routing and endpoint dispatch, not the response itself. Removing UseAuthorization carries a matching change: the same endpoint is mapped without .RequireAuthorization(), since EndpointMiddleware throws if an endpoint carries authorization metadata but no authorization middleware remains to enforce it — so that variant’s drop reflects the middleware’s absence and the endpoint’s own metadata together. Every request is pinned to https://example.test/, outside UseHsts’s default excluded hosts, so UseHsts genuinely writes a Strict-Transport-Security header rather than doing nothing.
Ranked by the drop in allocation each removal produced:
| Component removed | Δ Allocated (B/request) |
|---|---|
UseRouting |
−2345.10 |
UseAuthorization |
−2321.42 |
UseAuthentication |
−1296.94 |
UseHsts |
−433.70 |
UseExceptionHandler |
−1.80 |
UseHttpsRedirection |
−1.42 |
By this table, cutting routing saves more than cutting authorization — the opposite of what a name-based guess would predict. That ranking is also wrong.
Why the top of the table is misleading
AuthorizationMiddleware only evaluates a policy when the current endpoint carries one. With no endpoint selected — exactly what happens once UseRouting and its UseEndpoints registration are gone — the middleware finds a null policy and calls the next delegate directly, without evaluating anything. Removing UseRouting therefore switches off endpoint dispatch and authorization policy evaluation in the same step, and the −2345.10 B figure charges both to routing’s line above.
A seventh variant, removing UseRouting and UseAuthorization together, settles the question: it allocates 9440.61 B/request against NoRouting alone at 9440.95 B/request — a 0.34 B difference. Taking UseAuthorization out of an already-routingless pipeline changes almost nothing, because authorization was already doing no work there. Summing the two leave-one-out deltas predicts a combined saving of 4666.52 B; the measured saving is 2345.44 B, wrong by a factor of two. The two costs overlap almost completely, and the overlap is authorization’s.
Comparing NoAuthorization (routing present, authorization absent: 9464.63 B/request) against the combined variant (neither present: 9440.61 B/request) isolates routing’s own cost: about 24 bytes, measured against a terminal delegate that itself writes a response, not against nothing.
The corrected ranking
| Component | Marginal cost |
|---|---|
UseAuthorization |
roughly 2.3 KB (authorization’s work once an endpoint is selected: policy evaluation plus the endpoint’s authorization metadata) |
UseAuthentication |
roughly 1.27 KB |
UseHsts |
roughly 0.42 KB |
UseRouting |
roughly 24 B (net of the terminal delegate it replaces) |
UseHttpsRedirection / UseExceptionHandler |
effectively zero — under 2 B/request, sign not stable across runs |
This is close to what the middleware names already suggest, with three qualifications the names do not carry on their own. UseAuthorization costs roughly 1.8 times what UseAuthentication does, even with a no-op authentication scheme and an always-allow policy — authorization’s work once an endpoint is selected, including policy evaluation and the endpoint’s own metadata, allocates more than running the handler that authenticated the request. That cost is conditional: it exists only once routing selects an endpoint carrying authorization metadata, and is zero when none is selected — an endpoint with no authorization metadata was not measured here, so no claim is made about it. That conditionality is why it was possible to mistake authorization’s cost for routing’s. And UseHsts is not free — 0.42 KB for one response header is the only non-trivial cost among the three “transport” components, ahead of UseHttpsRedirection and UseExceptionHandler, which round to zero.
Why allocation, not wall clock
Allocated held for the full pipeline and the six leave-one-out variants across three runs, including one on an idle host with no other container competing for the machine: the largest movement was 0.01 KB. The combined variant that separates routing’s cost from authorization’s, above, was measured once, in the third run. Mean is not used here at all — its ratio against the full pipeline flipped direction for half the components between runs, NoExceptionHandler from 1.20 to 0.91 and NoHttpsRedirection from 1.10 to 0.92, on a harness BenchmarkDotNet itself flagged for sub-100 millisecond iterations and a multimodal distribution on several variants. A number that will not hold its sign is not a finding.
Program.cs is a shorter document with this table next to it. Every app.Use...() call in a pipeline is either measured somewhere or it is not, and most of them, in most codebases, have never been isolated the way UseAuthorization was here.
Measured on .NET SDK 10.0.401 / runtime 10.0.12 (Microsoft.AspNetCore.App 10.0.12), Linux (Ubuntu 24.04) arm64, inside the project’s Docker sandbox — a deliberate departure from the macOS host used to measure this publication’s earlier slots. The container ran with no resource limits (nproc 12, cpu.max max 100000, memory.max max), and no other container ran on the host during measurement. Neither ASPNETCORE_ENVIRONMENT nor DOTNET_ENVIRONMENT is set in the harness that builds and runs this container (lab/Dockerfile, lab/run.sh) — a property of the run scripts, not a value env.txt captured — and the generic HostBuilder does not auto-insert UseDeveloperExceptionPage in any environment regardless. The pipeline measured is the fixed six-component list above, in order, against requests pinned to https://example.test/ with HttpsRedirectionOptions.HttpsPort fixed to 443. What this harness cannot see: the Docker Desktop VM’s own CPU and memory allocation, or the host machine’s physical CPU model — BenchmarkDotNet reports that line as - for the same reason.
Frequently asked
Questions this raises
Why did removing UseRouting look like the biggest saving?
Because UseRouting and UseAuthorization are not independent in this pipeline. Removing routing also removes the endpoint metadata that UseAuthorization needs before it evaluates a policy, so NoRouting's 2.29 KB drop silently included authorization's own cost.
What does UseAuthorization actually cost once the overlap is removed?
About 2.3 KB per request for authorization's work once an endpoint is selected — evaluating a single always-allow policy plus the endpoint's own authorization metadata, against a no-op authentication scheme — the largest single component in this pipeline. An endpoint with no authorization metadata at all was not a variant here, so its cost is not claimed.
Why is wall-clock time not used to rank these middleware?
Because Mean and its Ratio against the full pipeline flipped direction for half the components between three otherwise identical runs, on a harness BenchmarkDotNet itself flagged for sub-100 millisecond iterations. Allocated stayed within 0.01 KB of itself across the same three runs.


