Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,26 @@ built-in support for basic authentication, JWT authentication, and OAuth2 authen
In addition to that, supports both the HTTP/1.1 and HTTP2 protocols and connection keep-alive, content
chunking, HTTP caching, data compression/decompression, payload binding, and authorization can be highlighted as the features of a `Service`.

#### HTTP/2 Stream Concurrency
Comment thread
daneshk marked this conversation as resolved.
Outdated

Each HTTP/2 connection can carry multiple requests simultaneously using independent streams. To prevent a single
connection from opening an unbounded number of streams and exhausting server memory, the listener enforces a limit
of **100 concurrent streams per connection** by default. This is the value recommended by RFC 7540 and consistent
with other widely-used HTTP/2 server implementations such as Nginx and Envoy.

When a client reaches this limit on a connection, it will automatically open an additional connection rather than
stalling, so normal traffic is unaffected for typical workloads.

The server-side stream limit can be tuned per listener using `http2MaxActiveStreams` in `ListenerConfiguration`:

```ballerina
listener http:Listener h2Listener = new (9090, {
http2MaxActiveStreams: 500
});
```

When not set, the listener defaults to 100.

## Issues and projects

Issues and Projects tabs are disabled for this repository as this is part of the Ballerina Standard Library. To report bugs, request new features, start new discussions, view project boards, etc. please visit Ballerina Standard Library [parent repository](https://github.com/ballerina-platform/ballerina-standard-library).
Expand Down
20 changes: 20 additions & 0 deletions ballerina/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,23 @@ built-in support for basic authentication, JWT authentication, and OAuth2 authen

In addition to that, supports both the HTTP/1.1 and HTTP2 protocols and connection keep-alive, content
chunking, HTTP caching, data compression/decompression, payload binding, and authorization can be highlighted as the features of a `Service`.

#### HTTP/2 Stream Concurrency

Each HTTP/2 connection can carry multiple requests simultaneously using independent streams. To prevent a single
Comment thread
daneshk marked this conversation as resolved.
Outdated
connection from opening an unbounded number of streams and exhausting server memory, the listener enforces a limit
of **100 concurrent streams per connection** by default. This is the value recommended by RFC 7540 and consistent
with other widely-used HTTP/2 server implementations such as Nginx and Envoy.

When a client reaches this limit on a connection, it will automatically open an additional connection rather than
stalling, so normal traffic is unaffected for typical workloads.

The server-side stream limit can be tuned per listener using `http2MaxActiveStreams` in `ListenerConfiguration`:

```ballerina
listener http:Listener h2Listener = new (9090, {
http2MaxActiveStreams: 500
});
```

When not set, the listener defaults to 100.
3 changes: 3 additions & 0 deletions ballerina/http_service_endpoint.bal
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ public type Local record {|
# + gracefulStopTimeout - Grace period of time in seconds for listener gracefulStop
# + socketConfig - Provides settings related to server socket configuration
# + http2InitialWindowSize - Configuration to change the initial window size in HTTP/2
# + http2MaxActiveStreams - Maximum concurrent HTTP/2 streams per connection advertised to clients.
# Defaults to 100
# + minIdleTimeInStaleState - Minimum time in seconds for a connection to be kept open which has received a GOAWAY.
# This only applies for HTTP/2. Default value is 5 minutes. If the value is set to -1,
# the connection will be closed after all in-flight streams are completed
Expand All @@ -164,6 +166,7 @@ public type ListenerConfiguration record {|
decimal gracefulStopTimeout = DEFAULT_GRACEFULSTOP_TIMEOUT;
ServerSocketConfig socketConfig = {};
int http2InitialWindowSize = 65535;
int http2MaxActiveStreams = 100;
decimal minIdleTimeInStaleState = 300;
decimal timeBetweenStaleEviction = 30;
|};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,8 @@ public final class HttpConstants {
public static final BString ENDPOINT_CONFIG_GRACEFUL_STOP_TIMEOUT = StringUtils.fromString("gracefulStopTimeout");
public static final BString ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE = StringUtils
.fromString("http2InitialWindowSize");
public static final BString ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS = StringUtils
.fromString("http2MaxActiveStreams");
public static final BString ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE = StringUtils.fromString(
"minIdleTimeInStaleState");
public static final BString ENDPOINT_CONFIG_TIME_BETWEEN_STALE_CHECK_RUNS = StringUtils.fromString(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
import static io.ballerina.stdlib.http.api.HttpConstants.ANN_CONFIG_ATTR_SSL_ENABLED_PROTOCOLS;
import static io.ballerina.stdlib.http.api.HttpConstants.CREATE_INTERCEPTORS_FUNCTION_NAME;
import static io.ballerina.stdlib.http.api.HttpConstants.ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE;
import static io.ballerina.stdlib.http.api.HttpConstants.ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS;
import static io.ballerina.stdlib.http.api.HttpConstants.HTTP_HEADERS;
import static io.ballerina.stdlib.http.api.HttpConstants.RESOLVED_REQUESTED_URI;
import static io.ballerina.stdlib.http.api.HttpConstants.RESPONSE_CACHE_CONTROL;
Expand Down Expand Up @@ -1583,6 +1584,8 @@ public static ListenerConfiguration getListenerConfig(long port, BMap endpointCo
listenerConfiguration.setPipeliningEnabled(true); //Pipelining is enabled all the time
listenerConfiguration.setHttp2InitialWindowSize(endpointConfig
.getIntValue(ENDPOINT_CONFIG_HTTP2_INITIAL_WINDOW_SIZE).intValue());
listenerConfiguration.setHttp2MaxConcurrentStreams(

@YasanPunch YasanPunch Jul 7, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In the pr description, it says: "Set to -1 for unlimited streams (not recommended for public-facing services)"

Where is this logic handled?

(int) endpointConfig.getIntValue(ENDPOINT_CONFIG_HTTP2_MAX_ACTIVE_STREAMS));

double minIdleTimeInStaleState =
((BDecimal) endpointConfig.get(HttpConstants.ENDPOINT_CONFIG_IDLE_TIME_STALE_STATE)).floatValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public static ListenerConfiguration getDefault() {
private boolean socketReuse;
private boolean socketKeepAlive;
private int http2InitialWindowSize = 65535;
private int http2MaxActiveStreams;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
private long minIdleTimeInStaleState = 3000000;
private long timeBetweenStaleEviction = 30000;

Expand Down Expand Up @@ -285,6 +286,14 @@ public void setHttp2InitialWindowSize(int http2InitialWindowSize) {
this.http2InitialWindowSize = http2InitialWindowSize;
}

public int getHttp2MaxConcurrentStreams() {
return http2MaxActiveStreams;
}

public void setHttp2MaxConcurrentStreams(int http2MaxActiveStreams) {
this.http2MaxActiveStreams = http2MaxActiveStreams;
}

public void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) {
this.timeBetweenStaleEviction = timeBetweenStaleEviction;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ public ServerConnector createServerConnector(ServerBootstrapConfiguration server
if (Constants.HTTP_2_0.equals(listenerConfig.getVersion())) {
serverConnectorBootstrap.setHttp2Enabled(true);
serverConnectorBootstrap.setHttp2InitialWindowSize(listenerConfig.getHttp2InitialWindowSize());
serverConnectorBootstrap.setHttp2MaxConcurrentStreams(listenerConfig.getHttp2MaxConcurrentStreams());
serverConnectorBootstrap.setMinIdleTimeInStaleState(listenerConfig.getMinIdleTimeInStaleState());
serverConnectorBootstrap.setTimeBetweenStaleEviction(listenerConfig.getTimeBetweenStaleEviction());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public class HttpServerChannelInitializer extends ChannelInitializer<SocketChann
private EventExecutorGroup pipeliningGroup;
private boolean webSocketCompressionEnabled;
private int http2InitialWindowSize;
private int http2MaxActiveStreams = 100;
private long minIdleTimeInStaleState;
private long timeBetweenStaleEviction;
private final BlockingQueue<Http2SourceHandler> http2StaleSourceHandlers = new LinkedBlockingQueue<>();
Expand Down Expand Up @@ -261,7 +262,7 @@ private void configureH2cPipeline(ChannelPipeline pipeline) {
// Add handler to handle http2 requests without an upgrade
pipeline.addLast(new Http2WithPriorKnowledgeHandler(
interfaceId, serverName, serverConnectorFuture, this, allChannels, listenerChannels,
reqSizeValidationConfig.getMaxHeaderSize(), http2InitialWindowSize));
reqSizeValidationConfig.getMaxHeaderSize(), http2InitialWindowSize, http2MaxActiveStreams));
// Add http2 upgrade decoder and upgrade handler
final HttpServerCodec sourceCodec = new HttpServerCodec(reqSizeValidationConfig.getMaxInitialLineLength(),
reqSizeValidationConfig.getMaxHeaderSize(),
Expand All @@ -281,7 +282,7 @@ private void configureH2cPipeline(ChannelPipeline pipeline) {
new Http2SourceConnectionHandlerBuilder(
interfaceId, serverConnectorFuture, serverName, this,
this.allChannels, this.listenerChannels, reqSizeValidationConfig.getMaxHeaderSize(),
this.http2InitialWindowSize).build());
this.http2InitialWindowSize, this.http2MaxActiveStreams).build());
} else {
return null;
}
Expand Down Expand Up @@ -355,6 +356,10 @@ void setHttp2InitialWindowSize(int http2InitialWindowSize) {
this.http2InitialWindowSize = http2InitialWindowSize;
}

void setHttp2MaxConcurrentStreams(int http2MaxActiveStreams) {
this.http2MaxActiveStreams = http2MaxActiveStreams;
}

void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) {
this.timeBetweenStaleEviction = timeBetweenStaleEviction;
}
Expand Down Expand Up @@ -447,7 +452,7 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
new Http2SourceConnectionHandlerBuilder(
interfaceId, serverConnectorFuture, serverName, channelInitializer,
allChannels, listenerChannels, reqSizeValidationConfig.getMaxHeaderSize(),
http2InitialWindowSize).build());
http2InitialWindowSize, http2MaxActiveStreams).build());
} else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
// handles pipeline for HTTP/1.x requests after SSL handshake
configureHttpPipeline(ctx.pipeline(), Constants.HTTP_SCHEME);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ public void setHttp2InitialWindowSize(int http2InitialWindowSize) {
httpServerChannelInitializer.setHttp2InitialWindowSize(http2InitialWindowSize);
}

public void setHttp2MaxConcurrentStreams(int http2MaxActiveStreams) {
httpServerChannelInitializer.setHttp2MaxConcurrentStreams(http2MaxActiveStreams);
}

public void setTimeBetweenStaleEviction(long timeBetweenStaleEviction) {
httpServerChannelInitializer.setTimeBetweenStaleEviction(timeBetweenStaleEviction);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ public Http2SourceConnectionHandlerBuilder(String interfaceId, ServerConnectorFu
ChannelGroup allChannels,
ChannelGroup listenerChannels,
long maxHeaderListSize,
int initialWindowSize) {
int initialWindowSize,
int maxActiveStreams) {
this.interfaceId = interfaceId;
this.serverConnectorFuture = serverConnectorFuture;
this.serverName = serverName;
Expand All @@ -63,6 +64,7 @@ public Http2SourceConnectionHandlerBuilder(String interfaceId, ServerConnectorFu
this.listenerChannels = listenerChannels;
this.initialSettings().maxHeaderListSize(maxHeaderListSize);
this.initialSettings().initialWindowSize(initialWindowSize);
this.initialSettings().maxConcurrentStreams(maxActiveStreams);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,16 @@ public class Http2WithPriorKnowledgeHandler extends ChannelInboundHandlerAdapter
private ChannelGroup listenerChannels;
private long maxHeaderListSize;
private int initialWindowSize;
private int maxActiveStreams;

public Http2WithPriorKnowledgeHandler(String interfaceId, String serverName,
ServerConnectorFuture serverConnectorFuture,
HttpServerChannelInitializer serverChannelInitializer,
ChannelGroup allChannels,
ChannelGroup listenerChannels,
long maxHeaderListSize,
int initialWindowSize) {
int initialWindowSize,
int maxActiveStreams) {
this.interfaceId = interfaceId;
this.serverName = serverName;
this.serverConnectorFuture = serverConnectorFuture;
Expand All @@ -65,6 +67,7 @@ public Http2WithPriorKnowledgeHandler(String interfaceId, String serverName,
this.listenerChannels = listenerChannels;
this.maxHeaderListSize = maxHeaderListSize;
this.initialWindowSize = initialWindowSize;
this.maxActiveStreams = maxActiveStreams;
}

@Override
Expand All @@ -83,7 +86,8 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) {
Constants.HTTP2_SOURCE_CONNECTION_HANDLER,
new Http2SourceConnectionHandlerBuilder(
interfaceId, serverConnectorFuture, serverName, serverChannelInitializer,
allChannels, listenerChannels, maxHeaderListSize, initialWindowSize).build());
allChannels, listenerChannels, maxHeaderListSize, initialWindowSize,
maxActiveStreams).build());
safelyRemoveHandlers(pipeline, Constants.HTTP2_UPGRADE_HANDLER,
Constants.HTTP_COMPRESSOR, Constants.HTTP_TRACE_LOG_HANDLER);
}
Expand Down
Loading
Loading