Skip to content

Expose HTTP.sys kernel response buffering control for HttpListener on Windows#124720

Open
karimsalem1 wants to merge 3 commits intodotnet:mainfrom
karimsalem1:main
Open

Expose HTTP.sys kernel response buffering control for HttpListener on Windows#124720
karimsalem1 wants to merge 3 commits intodotnet:mainfrom
karimsalem1:main

Conversation

@karimsalem1
Copy link

@karimsalem1 karimsalem1 commented Feb 22, 2026

This PR adds an opt-in AppContext switch to enable kernel response buffering in HttpListener (Windows implementation only). When enabled, the flag HTTP_SEND_RESPONSE_FLAG_BUFFER_DATA is set on all calls to HttpSendHttpResponse and HttpSendResponseEntityBody (consistent with Win32 docs). This mirrors the behavior for setting the HttpSysOptions.EnableKernelResponseBuffering flag in ASP.NET HTTP.sys code.

Motivation in issue #123425.

Sample usage:

AppContext.SetSwitch("System.Net.HttpListener.EnableKernelResponseBuffering", true);

using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8080/");
listener.Start();

Console.WriteLine("Listening...");
while (true)
{
    var context = await listener.GetContextAsync().ConfigureAwait(false);
    _ = Task.Run(() => HandleRequestAsync(context));
}

Notes:

  • No public API changes
  • Default behavior remains unchanged
  • Since API is internal, tests use reflection to validate flag behavior.
  • Added dedicated Windows-only test classes for HttpListener and HttpResponseStream, and consolidated existing Windows-only tests there for consistency.

Repro and validation

Setup:

  • Client: Azure VM in West US 2
  • Server: Azure VM in Australia East

Client uses HttpClient to send requests to the server and measures total latency. Server is running HttpListener and returns 7MB responses while varying the server-side response buffer size (from 32KB to 7MB). Server also records avg timing per individual response Write() call.

Benchmark

Kernel Buffering Disabled (Existing Behavior)

Buffer Size (bytes) Latency (ms) Write Time (µs)
32768 33911 158414
65536 16923 158108
131072 8609 159346
262144 4323 159938
524288 2296 163629
1048576 1181 168004
2097152 701 173846
4194304 468 206004
7000000 492 384459

Kernel Buffering Enabled (New Behavior)

Buffer Size (bytes) Latency (ms) Write Time (µs)
32768 1154 4589
65536 1149 9211
131072 1142 18205
262144 1107 35590
524288 1137 69975
1048576 686 75492
2097152 386 92539
4194304 451 109173
7000000 490 2356
  • Enabling HttpListener kernel buffering reduces E2E latency by up to 30x (34s → 1s for 32KB buffers)
  • Average Write() time drops significantly (158,414 µs → 4,589 µs at 32 KB).

These results confirm that without kernel buffering, small writes cause RTT amplification. Enabling kernel buffering eliminates small-write fragmentation over the wire and dramatically reduces latency.

Packet Capture

Packet analysis

Without kernel buffering, when the server sends a 7MB response using a 32KB buffer, packet capture shows that each server Write() results in a 32KB TCP segment (Length=32768, [PSH, ACK]). Each segment is transmitted approx. 1 RTT apart (~0.158s matching Azure-advertised RTT b/w the two regions). Transmission is effectively ACK-gated.

image

E2E the request takes ~34s:

image

With kernel buffering enabled, the TCP stack transmits large bursts of packets rather than one per RTT.

image

E2E latency dropped from ~34s to ~1s (first request ~1.9s due to TCP slow start, subsequent requests stabilize at ~0.95s).

image

TCP stream graph

The diff in behavior is best visualized using the Stevens TCP stream graph:

Without kernel buffering, the graph shows each ~32 KB increment in response transmission is separated by approx. one RTT. The slope is shallow (slow rate of growth) and evenly spaced.

image

With kernel buffering enabled, the slope becomes significantly steeper. Multiple segments are transmitted back-to-back before the next RTT boundary.

image

This visualizes the core issue: without buffering, small writes are RTT-amplified. With buffering, HTTP.sys aggregates response data in kernel space and transmits in large bursts, eliminating per-write RTT pacing.

Repro code

Init HttpListener with EnableKernelResponseBuffering switch

var listener = new HttpListener();
if (enableBuffering)
{
    AppContext.SetSwitch(
        "System.Net.HttpListener.EnableKernelResponseBuffering",
        true);
}

listener.Prefixes.Add("http://+:80/");
listener.Start();

while (true)
{
    var context = await listener.GetContextAsync();

    try
    {
        long responseSize = long.Parse(context.Request.QueryString["size"]);
        int bufferSize = int.Parse(context.Request.QueryString["buffer"]);

        context.Response.StatusCode = 200;
        await SendListenerResponse(context, responseSize, bufferSize);
    }
    finally
    {
        context.Response.Close();
    }
}

Server multi-write response routine

var buf = new byte[bufferSize];

long totalWriteUs = 0;
int writeCount = 0;

long totalSent = 0;
while (totalSent < size)
{
    int writeSize = (int)Math.Min(size - totalSent, bufferSize);

    long start = Stopwatch.GetTimestamp();

    await stream.WriteAsync(buf.AsMemory(0, writeSize));

    long end = Stopwatch.GetTimestamp();
    totalWriteUs += (end - start) / 10;
    writeCount++;

    totalSent += writeSize;
}

long avgWriteUs = totalWriteUs / writeCount;

Thanks @ManickaP for all the help!

@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Feb 22, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @karelz, @dotnet/ncl
See info in area-owners.md if you want to be subscribed.

@jkotas
Copy link
Member

jkotas commented Feb 22, 2026

No public API changes

AppContext switches are de-facto public APIs, just a different kind.

Copy link
Member

@ManickaP ManickaP left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One comment otherwise LGTM. I'll await the manual validation results.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Net.Http community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants