Skip to content

fix: respect both local and remote HTTP/2 concurrent stream limits#141

Merged
benubois merged 3 commits into
ostinelli:masterfrom
katpadi:fix/stream-limit-exceeded
Apr 14, 2026
Merged

fix: respect both local and remote HTTP/2 concurrent stream limits#141
benubois merged 3 commits into
ostinelli:masterfrom
katpadi:fix/stream-limit-exceeded

Conversation

@katpadi

@katpadi katpadi commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Fixes #140

Problem

delayed_push_async raises HTTP2::Error::StreamLimitExceeded on the 101st concurrent push. The http-2 gem enforces 2 independent limits remote (APNs, typically 1000) and local (http-2 default, 100) but only the remote was checked, allowing stream creation past the local limit.

Solution

  • effective_max_concurrent_streams uses min(remote, local) so backpressure triggers before either limit is hit
  • New max_concurrent_streams: option on Connection.new for explicit override
  • Rescue StreamLimitExceeded in delayed_push_async as a safety net

Tests

  • Added unit tests and an integration test that reproduces the exact bug
  • scenario (server advertising 1000 streams, 101 concurrent pushes).

katpadi added 2 commits April 4, 2026 19:20
Introduce effective_max_concurrent_streams using min(remote, local default)
to prevent StreamLimitExceeded when remote limit exceeds http-2's local
default of 100. Adds max_concurrent_streams: option for explicit override.

Fixes ostinelli#140
Reproduces the bug from ostinelli#140 by sending 101 async pushes against a server
advertising 1000 streams. Also makes dummy server max_concurrent_streams
configurable and processes stream responses concurrently.
@k-tsuchiya-jp

Copy link
Copy Markdown

Hi @katpadi,

Thanks for the PR — I tested this on my side and confirmed that the original issue is resolved with your changes.

Previously, we were able to reproduce HTTP2::Error::StreamLimitExceeded when sending more than 100 concurrent async pushes, due to the mismatch between the http-2 local limit (100) and the higher peer limit (e.g. 1000 from APNs).
With this fix, that error no longer occurs in our environment.

The added spec also makes sense to me:

  • the server advertises a higher concurrent stream limit (e.g. 1000)
  • more than 100 async notifications are sent concurrently
  • streams are intentionally kept in-flight to expose the old local limit behavior

This matches the production scenario we observed quite well.

The changes to the dummy server (allowing configurable settings_max_concurrent_streams and handling requests in a background thread) also look appropriate to properly simulate concurrent streams.

From my perspective, this fix looks correct and addresses the issue.

Thanks again for working on this!

@benubois

benubois commented Apr 7, 2026

Copy link
Copy Markdown
Collaborator

Hi @katpadi,

Amazing, thank you!

Is it necessary to expose max_concurrent_streams as part of the public API?

  • If there's not a good reason a user would set this let's remove it.
  • If you think the option should stay, I'm happy to leave it in, but it should be documented in the README.

The connection already negotiates correctly by taking the minimum of
the server-advertised limit and the http-2 local default. Exposing
this as a user option adds no value and could mask stream limit errors
if misconfigured.
@katpadi

katpadi commented Apr 8, 2026

Copy link
Copy Markdown
Contributor Author

@benubois - yeah, I agree. I removed max_concurrent_streams from the public API. There's no scenario where a user would need to override this. Exposing it only risks masking errors if someone sets it too high.

@benubois benubois merged commit 77d7ec1 into ostinelli:master Apr 14, 2026
8 checks passed
@benubois

Copy link
Copy Markdown
Collaborator

Thanks again!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StreamLimitExceeded with http-2 v1.x due to local vs remote concurrent stream limit mismatch

3 participants