HTTP/2 allows an application to send multiple requests over a single connection. This capability is useful for .NET applications that communicate with APIs, microservices, and gRPC services because it reduces the need to establish a separate connection for every request.

However, the server controls how many concurrent HTTP/2 streams a connection can support. When a client opens a new connection, it may not know the server's stream limit until it receives the server's HTTP/2 SETTINGS frame. During this initial period, the client needs a default concurrency limit.

.NET 11 RC 2 introduces the SocketsHttpHandler.InitialHttp2MaxConcurrentStreams property, which allows developers to configure that initial limit. This is particularly useful when the server's stream capacity is known to be lower than the default of 100.

In this article, we will examine how HTTP/2 concurrency works, configure the new property in C#, and discuss when changing the initial limit makes sense for production applications.

Understanding HTTP/2 Stream Concurrency

HTTP/2 uses streams to carry individual request and response exchanges over a shared connection. Multiple streams can remain active at the same time, allowing requests to progress without requiring a separate TCP connection for each exchange.

For example, an ASP.NET Core application might call several backend services to assemble a response. If the application reuses an HttpClient, its HTTP/2 connection pool can reuse connections and multiplex requests over them.

The number of concurrent streams is important because the server advertises its supported limit through an HTTP/2 SETTINGS_MAX_CONCURRENT_STREAMS setting.

Consider a server that allows 50 concurrent streams per connection. If a client attempts to send a large burst of requests before receiving the server's settings, its initial concurrency assumptions may not match the server's actual capacity.

A lower initial limit helps control this early burst.

What's New in .NET 11?

.NET 11 RC 2 introduces the following property:

SocketsHttpHandler.InitialHttp2MaxConcurrentStreams

It controls the maximum number of concurrent HTTP/2 streams the client initially permits on each new connection, before the server's SETTINGS frame arrives.

The property accepts values greater than or equal to 1. Its default initial value is 100.

Once the client receives the server's advertised limit, the connection follows that limit. If the client has previously observed a lower limit for the same host, that information can further reduce the initial concurrency used for a new connection.

This behavior is important because the property does not override the server's concurrency policy. It configures the client's initial assumption while connection settings are being established.

How to Configure Initial HTTP/2 Concurrency

The property belongs to SocketsHttpHandler, so developers configure it when creating the HTTP handler used by HttpClient.

Step 1: Create a SocketsHttpHandler

The following example sets the initial concurrent-stream limit to 32.

using System.Net.Http;

var handler = new SocketsHttpHandler
{
    InitialHttp2MaxConcurrentStreams = 32
};

using var client = new HttpClient(handler);

The handler controls HTTP connection behavior. Setting the property to 32 means that a new HTTP/2 connection initially permits up to 32 concurrent streams before the server's settings are received.

The value is an example, not a universal recommendation. Choose a value based on the server's known limits and the behavior of your application.

Step 2: Send HTTP/2 Requests

To make the example explicitly request HTTP/2, configure the request version and version policy.

using System.Net;
using System.Net.Http;

var handler = new SocketsHttpHandler
{
    InitialHttp2MaxConcurrentStreams = 32
};

using var client = new HttpClient(handler);

using var request = new HttpRequestMessage(
    HttpMethod.Get,
    "https://api.example.com/orders")
{
    Version = HttpVersion.Version20,
    VersionPolicy = HttpVersionPolicy.RequestVersionExact
};

using HttpResponseMessage response =
    await client.SendAsync(request);

response.EnsureSuccessStatusCode();

string content = await response.Content.ReadAsStringAsync();

Console.WriteLine(content);

The example uses a placeholder API address. Replace it with your actual endpoint.

Version20 requests HTTP/2, while RequestVersionExact requires the request to use that version rather than silently falling back to another HTTP version.

The EnsureSuccessStatusCode call ensures that unsuccessful HTTP responses are not treated as successful results.

For production code, also consider cancellation tokens, appropriate timeouts, structured logging, and application-specific error handling.

Step 3: Reuse HttpClient

For applications making repeated requests, reuse the HttpClient and its handler rather than constructing a new client for every operation.

A long-lived client helps reuse pooled connections and avoids unnecessary connection establishment.

In ASP.NET Core applications, IHttpClientFactory is another suitable option. Configure the handler when registering the named or typed client, then inject that client into the services that need it.

Choosing an Appropriate Concurrency Value

The right initial value depends on the server and the workload. A lower value may be useful when the server has a known low stream limit, but it can also restrict how many requests the client starts on a new connection before receiving the server's settings.

Initial value

Potential benefit

Potential drawback

1

Limits the initial burst to one stream

Can restrict initial request concurrency

16

Provides modest initial concurrency

May be unnecessarily restrictive for some workloads

32

Allows a larger initial burst than 16

May still exceed a server's actual capacity

100

Matches the default initial limit

May be higher than a particular server supports

These values are illustrative, not benchmark results. The best value depends on the actual server configuration and connection behavior.

A server can advertise a different limit after the connection is established. Therefore, choosing 16 or 32 does not guarantee that the connection will continue using that number of streams.

Practical Production Scenario

Consider a .NET service that sends many requests to an internal API. The API gateway is configured to support a maximum of 20 concurrent streams per HTTP/2 connection.

The client initially uses the default concurrency of 100. If the client begins a large burst before receiving the server's settings, it may initially attempt more concurrent streams than the gateway supports.

Setting the initial limit to 20 can align the client's initial behavior with the known server configuration.

var handler = new SocketsHttpHandler
{
    InitialHttp2MaxConcurrentStreams = 20
};

var client = new HttpClient(handler);

This configuration is useful only if the assumed server limit is accurate and applicable to the connection.

It does not guarantee a specific throughput improvement. Network latency, server processing capacity, connection establishment, request duration, and application-level throttling can all influence performance.

If the service communicates with multiple servers that advertise different limits, avoid assuming that one manually selected value is optimal for every destination.

Initial HTTP/2 Concurrency Compared With Other Controls

Several HTTP settings influence request behavior, but they solve different problems.

Setting or mechanism

What it controls

InitialHttp2MaxConcurrentStreams

Initial stream concurrency on a new HTTP/2 connection

Server-advertised HTTP/2 stream limit

The concurrency limit communicated by the server

EnableMultipleHttp2Connections

Whether additional HTTP/2 connections can be opened to the same server when needed

Application-level concurrency limit

How much work the application starts concurrently

Request timeout

How long an operation may run before cancellation or failure

These controls should not be treated as interchangeable.

For example, limiting the initial number of streams does not cap the total number of requests your application can execute across multiple connections. If the application must protect a downstream database or API from excessive load, an application-level concurrency limit may also be necessary.

Similarly, enabling multiple HTTP/2 connections is a separate decision. It can allow additional connections when concurrency on an existing connection is insufficient, but it also introduces additional connection-management considerations.

Advantages and Disadvantages

Advantages

More control over connection startup: Developers can reduce the initial stream count when the server's limit is known.

Better alignment with server capacity: A lower initial value can help avoid an unnecessarily large initial request burst.

Simple configuration: The property can be set directly on SocketsHttpHandler without implementing a custom HTTP transport.

Compatibility with existing clients: The setting works within the standard .NET HTTP client architecture.

Disadvantages

No guaranteed performance improvement: A lower limit may reduce initial concurrency and delay requests in some workloads.

Server settings still apply: The client cannot use this property to override the server's advertised concurrency limit.

Workload-specific tuning: Different servers and applications may require different values.

Limited scope: The property controls initial connection behavior, not total application concurrency or overall request throughput.

Best Practices and Troubleshooting

1. Confirm That HTTP/2 Is Actually Being Used

Check the negotiated protocol when diagnosing concurrency behavior. A request configured to prefer HTTP/2 may use another version if the version policy permits fallback.

For controlled testing, use RequestVersionExact when you need to ensure that a request uses HTTP/2.

2. Start With the Server's Known Limit

If the server advertises a documented stream limit, use that information to select a reasonable initial value.

Do not choose a low value simply because it appears safer. Excessively restricting concurrency can create unnecessary waiting when the server supports more streams.

3. Measure Before and After Changing the Setting

Test under representative traffic conditions. Observe request latency, throughput, connection counts, server-side errors, and resource consumption.

Compare equivalent workloads and avoid attributing a performance change to this property without considering other factors.

4. Investigate Unexpected Concurrency

If fewer requests run concurrently than expected, inspect the server's advertised limit, connection reuse, and any application-level throttling.

Also check whether multiple HTTP/2 connections are enabled and whether requests are reaching the intended endpoint.

5. Avoid Creating a New Client for Every Request

Repeatedly creating clients can prevent effective connection reuse and introduce unnecessary connection overhead.

Use a long-lived client or IHttpClientFactory and configure the handler consistently.

Summary

.NET 11 RC 2 introduces SocketsHttpHandler.InitialHttp2MaxConcurrentStreams, allowing developers to configure the initial HTTP/2 stream concurrency limit before the server's settings arrive.

The property is useful when the server's supported stream limit is known to be lower than the default initial value of 100. It helps align early client behavior with the expected server capacity without changing the server's own concurrency policy.

For production applications, choose the value based on documented server limits and measured workload behavior. Combine it with proper HttpClient reuse, application-level concurrency controls, and performance monitoring to build a more predictable HTTP communication strategy.