Skip to content

[http2_adapter] Flow-control window is not configurable, causing 21x slower large-body downloads over HTTP/2 vs HTTP/1.1 #2606

Open

Description

@AlexV525

Package

http2_adapter

Version

2.9.0

Operating-System

Android, iOS, MacOS, Linux, Windows

Adapter

Http2Adapter

Output of flutter doctor -v

Dart Version

No response

Steps to Reproduce

  1. Configure a Dio instance with Http2Adapter(ConnectionManager()) against a TLS endpoint that negotiates h2.
  2. Download a resource ≥ 1 MiB (e.g. 10 MiB) from that endpoint.
  3. Compare wall-clock throughput against the same endpoint/payload served over HTTP/1.1 with the default IOHttpClientAdapter.

Minimal repro sketch:

final dio = Dio()..httpClientAdapter = Http2Adapter(ConnectionManager());
final sw = Stopwatch()..start();
await dio.get<List<int>>(
  'https://example.com/10mb-payload',
  options: Options(responseType: ResponseType.bytes),
);
sw.stop();
print('h2 10MiB download: ${sw.elapsedMilliseconds} ms');

Run the same request through IOHttpClientAdapter() (HTTP/1.1) against the identical route/payload and compare.

Expected Result

Large-body downloads over HTTP/2 should perform comparably to (or reasonably close to) HTTP/1.1 on the same connection quality, since HTTP/2 is expected to be at least as capable for single large transfers.

Actual Result

In a controlled same-route, same-payload, ALPN-only-differing benchmark (Dart VM tier, 3 independent runs, ⁣results in research-dio-adapters/bench/results-summary.md):

Scenario HTTP/1.1 (IOHttpClientAdapter) HTTP/2 (Http2Adapter) Ratio
32-concurrent small requests 5818.55 req/s 9772.78 req/s h2 +68% (expected win)
1 MiB download 207.91 MiB/s 95.06 MiB/s h1 2.2× faster
10 MiB download 231.43 MiB/s 10.81 MiB/s h1 21.4× faster
1 MiB upload 212.32 MiB/s 165.55 MiB/s h1 +28%

Root cause: _ConnectionManager._createSocket calls ClientTransportConnection.viaSocket(socket) without a third settings argument (plugins/http2_adapter/lib/src/connection_manager_imp.dart), so every connection uses package:http2's default ClientSettings() — a 65535-byte initial per-stream flow-control window. A 10 MiB body needs roughly 160 round trips of WINDOW_UPDATE before the peer is allowed to send more data; that RTT-bound ping-pong is the entire gap. Confirmed by control experiment: driving the same server with a raw package:http2 client that advertises a 32 MiB window brings the same 10 MiB transfer down to ~91 ms.

Neither ConnectionManager's public factory (plugins/http2_adapter/lib/src/connection_manager.dart) nor ClientSetting (plugins/http2_adapter/lib/src/client_setting.dart) exposes any way to override the flow-control window or concurrentStreamLimit. There is no user-facing workaround short of forking the package.

Solution Brainstorm (proposed fix)

Add an optional settings override to ConnectionManager's public factory and thread it through to viaSocket:

factory ConnectionManager({
  Duration idleTimeout = const Duration(seconds: 15),
  Duration? handshakeTimeout,
  void Function(Uri uri, ClientSetting)? onClientCreate,
  ProxyConnectedPredicate proxyConnectedPredicate = defaultProxyConnectedPredicate,
  List<String> supportedProtocols = const ['h2'],
  // NEW:
  int? streamWindowSize,
  int? concurrentStreamLimit,
}) => _ConnectionManager(
  ...,
  clientSettings: ClientSettings(
    streamWindowSize: streamWindowSize,
    concurrentStreamLimit: concurrentStreamLimit,
  ),
);

And in _createSocket/the connect path:

final transport = ClientTransportConnection.viaSocket(socket, settings: _clientSettings);

Default behavior (no window override) stays byte-for-byte identical, so this is non-breaking. Suggested doc note on the new parameters: "Increase streamWindowSize for workloads that mix large downloads with the same Http2Adapter instance; the default 65535-byte window can make large transfers dramatically slower than HTTP/1.1 due to flow-control round trips."

Additional context

Two related, longer-standing upstream issues in package:http2 describe the same window-update cost from a different angle and would benefit from this fix landing on the dio side regardless of upstream timeline:

  • dart-lang/http#1365 "http2 seems to be slow" (2020)
  • dart-lang/http#1384 "Performance checks and enhancements" (2024)

Full benchmark methodology, harness source, and three independent raw result files are available on request (Dart VM tier: 5 candidates × 3 rounds, all failures fields empty; Android real-device tier corroborates the qualitative direction but with different absolute numbers due to adb reverse latency skew — see accompanying research write-up if useful for the PR description).

Activity

  1. self-assigned this
    on Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

e: performanceImprovements or additions to performancep: http2_adapterTargeting `http2_adapter` packageplatform: ios: bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions