Diagnostics Allocations in ASP.NET Core
10/12/2025 | 4 minutes to read
Object Allocations
ASP.NET Core's Kestrel is optimized for high performance and scale. To achieve high performance, it reduces heap allocations by either pooling large objects or by allocating struct
s on the stack. By reducing allocations, the GC has less work to do. As pooled objects get promoted to Gen2 generation, the most common collections (Gen0 and Gen1) become cheaper as they contain fewer objects to handle. However, pooling is not entirely free:
it increases the Gen2 size and its corresponding collections
cross-generation references may require tracking references from Gen2 regions pointing to lower generation regions (for example, when a pooled object contains a reference to a newly allocated object).
One example is type Http2Stream
(or Http3Stream
), which corresponds to a request-response pair in a connection. As such objects are large, they are typically pooled. These objects may have a reference to the corresponding HttpContext
, which should be also pooled, otherwise it incurs an allocation or a cross-generation reference.
Measuring Allocations per Request
We can measure allocations that correspond to a single request-response pair. The simplest way to correlate allocations to requests is by sending a large number of requests (say 20,000) and measuring each allocation. The most commonly allocated types are string
, byte[]
, or Task
, which should not be surprising in a web application. When objects of a given type are allocated at positive integer multiples of the requests, it reveals that at least one such object is allocated while serving each request.
While reviewing allocation metrics, string
s can be easily overlooked:
Applications and frameworks typically use many
string
s (for example, HTTP protocol isstring
-based)When
string
s are overlooked, one might miss Large Object Heap (LOH) allocations too, and those can cause performance issues.
HexConverter
During one such measurement, I noticed an interesting allocation in ASP.NET Core that surprised me, given the general efficiency of the framework. For each HTTP/2 request, a string
object gets allocated by HexConverter.ToString
.
The allocation happens in the stack trace shown below, which reveals that it belongs to a diagnostics/metrics/logging code path.
System.Private.CoreLib.dll!System.HexConverter.ToString(System.ReadOnlySpan<byte> bytes, System.HexConverter.Casing casing) Line 116 C# System.Diagnostics.DiagnosticSource.dll!System.Diagnostics.ActivityTraceId.CreateFromBytes(System.ReadOnlySpan<byte> idData) System.Diagnostics.DiagnosticSource.dll!System.Diagnostics.ActivityTraceId.CreateRandom() System.Diagnostics.DiagnosticSource.dll!System.Diagnostics.Activity.GenerateW3CId() System.Diagnostics.DiagnosticSource.dll!System.Diagnostics.Activity.Start() Microsoft.AspNetCore.Hosting.dll!Microsoft.AspNetCore.Hosting.HostingApplicationDiagnostics.StartActivity(Microsoft.AspNetCore.Http.HttpContext httpContext, bool loggingEnabled, bool diagnosticListenerActivityCreationEnabled, out bool hasDiagnosticListener) Microsoft.AspNetCore.Hosting.dll!Microsoft.AspNetCore.Hosting.HostingApplicationDiagnostics.BeginRequest(Microsoft.AspNetCore.Http.HttpContext httpContext, Microsoft.AspNetCore.Hosting.HostingApplication.Context context) Microsoft.AspNetCore.Hosting.dll!Microsoft.AspNetCore.Hosting.HostingApplication.CreateContext(Microsoft.AspNetCore.Http.Features.IFeatureCollection contextFeatures)
A string
is allocated for diagnostic traces for an object typed as Activity
.
With a closer look at the code, it turns out that the Activity
object is allocated, if the _activitySource
is subscribed to, logging is enabled, or _diagnosticListener
is subscribed to (in .NET 10 Preview 7).
Disable HexConverter Allocation
Both with the regular WebApplicationBuilder
or Slim builder, the allocation occurs by default. The stacktrace reveals that the allocation is made by the host, rather than the server itself. To remove this allocation, all 3 above conditions should be disabled.
In a default application this can be done by disabling the logging with the services in Program.cs:
builder.Services.AddLogging(logBuilder => { logBuilder.ClearProviders(); });
However, this solution has a downside: there are no more log messages output by the application. A different approach could be to only disable the diagnostics corresponding to these log messages. This can be done by setting "Microsoft.AspNetCore.Hosting.Diagnostics": "None"
in the appsettings.json:
"Logging": { "LogLevel": { "Microsoft.AspNetCore.Hosting.Diagnostics": "None" }, "EventLog": { "LogLevel": { "Microsoft.AspNetCore.Hosting.Diagnostics": "None" } } }
Conclusion
This investigation reveals how even small diagnostic operations — like creating an Activity
and using System.HexConverter.ToString
— can lead to per-request string
allocations. These are not a problem for most applications (as they likely allocate significantly more objects), moreover it is probably the preferred setting for good telemetry and observability. However, while rich diagnostics and automatic tracing enhance system observability, they come at the cost of increased allocations and potential cross-generation overhead. These are more apparent for performance-critical applications. This post showed, how such allocations may be disabled in ASP.NET Core at the cost of reduced observability.