From d87227860639449adbb3a8299fcb2681260caf88 Mon Sep 17 00:00:00 2001 From: Koen Rijpstra Date: Fri, 27 Jun 2025 14:09:01 +0200 Subject: [PATCH 1/4] fix(http2): bump connection window immediately after connect() If the user sets `grpc-node.connection_flow_control_window` to a value > 65 535 B, we now send a WINDOW_UPDATE (or setLocalWindowSize) right after `http2.connect()` returns. This removes the 65 KB start-window stall that caused large initial backlogs on high-throughput streams, especially when an H2 proxy (e.g. HAProxy) sat between client and server. Behaviour now matches Go/Rust gRPC; no API changes. --- packages/grpc-js/src/transport.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index ba0675d08..a93419ef8 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -722,6 +722,24 @@ export class Http2SubchannelConnector implements SubchannelConnector { http2.getDefaultSettings().initialWindowSize, } }); + + // Send WINDOW_UPDATE now to avoid 65 KB start-window stall. + const defaultWin = http2.getDefaultSettings().initialWindowSize; // 65 535 B + const connWin = options[ + 'grpc-node.connection_flow_control_window' + ] as number | undefined; + + if (connWin && connWin > defaultWin) { + try { + // Node ≥ 14.18 + (session as any).setLocalWindowSize(connWin); + } catch { + // Older Node: bump by the delta + const delta = connWin - (session.state.localWindowSize ?? defaultWin); + if (delta > 0) (session as any).incrementWindowSize(delta); + } + } + this.session = session; let errorMessage = 'Failed to connect'; let reportedError = false; From aeb7a5fd52e319887787a163c5ccfe852f53e35d Mon Sep 17 00:00:00 2001 From: Koen Rijpstra Date: Fri, 27 Jun 2025 14:35:21 +0200 Subject: [PATCH 2/4] fix(http2): handle default initial window size correctly --- packages/grpc-js/src/transport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index a93419ef8..084feff0e 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -724,7 +724,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { }); // Send WINDOW_UPDATE now to avoid 65 KB start-window stall. - const defaultWin = http2.getDefaultSettings().initialWindowSize; // 65 535 B + const defaultWin = http2.getDefaultSettings().initialWindowSize ?? 65535; // 65 535 B const connWin = options[ 'grpc-node.connection_flow_control_window' ] as number | undefined; From b69bcad1bb03772e451a785e1dabf07616481f67 Mon Sep 17 00:00:00 2001 From: Koen Rijpstra Date: Fri, 27 Jun 2025 15:11:56 +0200 Subject: [PATCH 3/4] fix(http2): rename connection flow control window option to grpc-node.flow_control_window --- packages/grpc-js/src/transport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index 084feff0e..c32b5b3dd 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -726,7 +726,7 @@ export class Http2SubchannelConnector implements SubchannelConnector { // Send WINDOW_UPDATE now to avoid 65 KB start-window stall. const defaultWin = http2.getDefaultSettings().initialWindowSize ?? 65535; // 65 535 B const connWin = options[ - 'grpc-node.connection_flow_control_window' + 'grpc-node.flow_control_window' ] as number | undefined; if (connWin && connWin > defaultWin) { From 0e09b9cd59042ec905ea7fec85e489d95e786729 Mon Sep 17 00:00:00 2001 From: Koen Rijpstra Date: Fri, 27 Jun 2025 21:38:50 +0200 Subject: [PATCH 4/4] fix(http2): move WINDOW_UPDATE handling to remoteSettings event --- packages/grpc-js/src/transport.ts | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/grpc-js/src/transport.ts b/packages/grpc-js/src/transport.ts index c32b5b3dd..47f6105df 100644 --- a/packages/grpc-js/src/transport.ts +++ b/packages/grpc-js/src/transport.ts @@ -723,28 +723,29 @@ export class Http2SubchannelConnector implements SubchannelConnector { } }); - // Send WINDOW_UPDATE now to avoid 65 KB start-window stall. + // Prepare window size configuration for remoteSettings handler const defaultWin = http2.getDefaultSettings().initialWindowSize ?? 65535; // 65 535 B const connWin = options[ 'grpc-node.flow_control_window' ] as number | undefined; - - if (connWin && connWin > defaultWin) { - try { - // Node ≥ 14.18 - (session as any).setLocalWindowSize(connWin); - } catch { - // Older Node: bump by the delta - const delta = connWin - (session.state.localWindowSize ?? defaultWin); - if (delta > 0) (session as any).incrementWindowSize(delta); - } - } this.session = session; let errorMessage = 'Failed to connect'; let reportedError = false; session.unref(); session.once('remoteSettings', () => { + // Send WINDOW_UPDATE now to avoid 65 KB start-window stall. + if (connWin && connWin > defaultWin) { + try { + // Node ≥ 14.18 + (session as any).setLocalWindowSize(connWin); + } catch { + // Older Node: bump by the delta + const delta = connWin - (session.state.localWindowSize ?? defaultWin); + if (delta > 0) (session as any).incrementWindowSize(delta); + } + } + session.removeAllListeners(); secureConnectResult.socket.removeListener('close', closeHandler); secureConnectResult.socket.removeListener('error', errorHandler);