Skip to content

feat: backpressure#408

Open
lazarv wants to merge 1 commit intomainfrom
feat/backpressure
Open

feat: backpressure#408
lazarv wants to merge 1 commit intomainfrom
feat/backpressure

Conversation

@lazarv
Copy link
Copy Markdown
Owner

@lazarv lazarv commented Apr 27, 2026

Hardens react-server start for production deployment behind a load balancer or a k8s/Docker orchestrator. The Node HTTP server now ships with sensible slow-loris and idle-connection timeouts, signals propagate correctly through the cluster master, workers drain in-flight requests on SIGTERM instead of dropping them, and a crash-loop guard prevents fork-bombing the host on a deterministic boot failure. A new readiness endpoint reports worker liveness so external probes can route around a dead worker before the kernel reaps the socket.

The headline addition is an adaptive admission controller built on performance.eventLoopUtilization() with an AIMD update loop. Under load it expands the in-flight limit while ELU stays below target and contracts multiplicatively when the loop saturates, holding tail latency well below the unbounded baseline. Admission is FIFO across the wait queue so requests don't starve, and a fast-path release skips the EWMA bookkeeping on the hot path. Backpressure is opt-in by default and auto-enables only under react-server start cluster mode — the feature is meaningful on Node, not on edge or serverless, and the import chain is gated accordingly. Both REACT_SERVER_BACKPRESSURE and a backpressure.enabled config key override the default in either direction.

The static-asset handler was rewritten to use async stat() with in-flight coalescing, a bounded pending map, and a bounded miss set, so a flood of unique paths can't blow the libuv thread pool or the heap. Hit/miss decisions stay synchronous in the steady state via a microtask-elision pattern that avoids a Promise.resolve round-trip on every request.

While exercising the HTTPS path under HTTP/2, two pre-existing bugs surfaced and were fixed in the same branch since the HTTPS surface is on the critical path. Building a WHATWG Request from req.headers under Node's HTTP/2 compat layer threw TypeError: Key Symbol(sensitiveHeaders) ... cannot be converted to a ByteString because Node tags headers with an internal symbol and adds :method/:path/:authority/:scheme pseudo-headers; the middleware now copies only string keys that don't begin with :. Separately, HTTP/1.1's keepAliveTimeout/headersTimeout/requestTimeout don't apply to HTTP/2 sessions, so an HTTP/2 slow-loris would have hung the worker indefinitely; a session-level setTimeout closes the gap. A related discovery — Node's default 30s connectionsCheckingInterval silently masks the configured timeouts — is fixed by passing a 5s interval to createServer() and exposing it as a config option.

The runtime had no defenses against partial requests, no graceful shutdown story under k8s (the master is PID 1; the OS doesn't propagate signals to workers), no concurrency ceiling under burst load, and no way for an orchestrator to learn that a worker had died before the listener socket closed. Each gap was independently capable of producing dropped requests, runaway latency, or a thundering crash-loop in production. This branch closes all of them and lays the groundwork for the load-shedding behavior that the upcoming docs page describes.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
react-server-docs e65b46a Apr 27 2026, 09:20 PM

@github-actions
Copy link
Copy Markdown

⚡ Flight Protocol Benchmark

Commit: 949f9ec

Serialization (renderToReadableStream)

Scenario @lazarv/rsc webpack vs webpack
react: minimal element 202.7K 26.4K 🟢 +668.1%
react: shallow wide (1000) 2.1K 341 🟢 +511.3%
react: deep nested (100) 17.2K 5.5K 🟢 +212.3%
react: product list (50) 6.2K 1.8K 🟢 +237.6%
react: large table (500x10) 279 103 🟢 +172.1%
data: primitives 177.0K 36.1K 🟢 +389.7%
data: large string (100KB) 7.2K 6.9K 🟢 +3.6%
data: nested objects (20) 58.6K 25.3K 🟢 +131.6%
data: large array (10K) 118 111 🟢 +6.3%
data: Map & Set 10.8K 5.6K 🟢 +92.8%
data: Date/BigInt/Symbol 156.6K 32.6K 🟢 +380.8%
data: typed arrays 33.1K 14.2K 🟢 +132.5%
data: mixed payload 8.5K 4.0K 🟢 +112.7%

Prerender (prerender)

Scenario @lazarv/rsc ops/s mean
react: minimal element 260.9K 3.8 µs
react: shallow wide (1000) 2.0K 500.1 µs
react: deep nested (100) 15.9K 63.1 µs
react: product list (50) 5.9K 168.5 µs
react: large table (500x10) 273 3.67 ms
data: primitives 196.1K 5.1 µs
data: large string (100KB) 691 1.45 ms
data: nested objects (20) 59.2K 16.9 µs
data: large array (10K) 118 8.47 ms
data: Map & Set 11.2K 89.1 µs
data: Date/BigInt/Symbol 185.8K 5.4 µs
data: typed arrays 671 1.49 ms
data: mixed payload 7.7K 129.6 µs

Deserialization (createFromReadableStream)

Scenario @lazarv/rsc webpack vs webpack
react: minimal element 168.3K 137.8K 🟢 +22.1%
react: shallow wide (1000) 24.1K 2.0K 🟢 +1098.2%
react: deep nested (100) 99.1K 19.2K 🟢 +416.4%
react: product list (50) 53.3K 14.6K 🟢 +265.5%
react: large table (500x10) 4.2K 2.2K 🟢 +90.6%
data: primitives 140.7K 126.3K 🟢 +11.4%
data: large string (100KB) 35.1K 35.8K 🔴 -2.0%
data: nested objects (20) 83.9K 65.8K 🟢 +27.5%
data: large array (10K) 284 225 🟢 +26.3%
data: Map & Set 16.7K 14.3K 🟢 +16.9%
data: Date/BigInt/Symbol 135.8K 109.3K 🟢 +24.2%
data: typed arrays 54.2K 41.1K 🟢 +32.0%
data: mixed payload 25.9K 13.2K 🟢 +95.8%

Roundtrip (serialize + deserialize)

Scenario @lazarv/rsc webpack vs webpack
react: minimal element 102.8K 20.6K 🟢 +399.1%
react: shallow wide (1000) 1.8K 283 🟢 +522.1%
react: deep nested (100) 13.7K 3.9K 🟢 +251.5%
react: product list (50) 5.2K 1.6K 🟢 +220.2%
react: large table (500x10) 264 91 🟢 +189.9%
data: primitives 79.6K 28.0K 🟢 +184.3%
data: large string (100KB) 6.4K 6.6K 🔴 -2.7%
data: nested objects (20) 33.5K 18.4K 🟢 +81.7%
data: large array (10K) 83 78 🟢 +6.6%
data: Map & Set 6.2K 3.9K 🟢 +58.3%
data: Date/BigInt/Symbol 71.5K 22.2K 🟢 +222.7%
data: typed arrays 23.4K 11.1K 🟢 +111.5%
data: mixed payload 5.4K 3.0K 🟢 +81.4%
Legend & methodology

Indicators: 🟢 > 1% faster | 🔴 > 1% slower | ⚪ within noise margin

vs webpack: compares @lazarv/rsc against react-server-dom-webpack within the same run.
vs baseline: compares @lazarv/rsc against the previous main branch run.

Values shown are operations/second (higher is better). Each scenario runs for at least 100 iterations with warmup.

Benchmarks run on GitHub Actions runners (shared infrastructure) — expect ~5% variance between runs. Consistent directional changes across multiple scenarios are more meaningful than any single number.

@github-actions
Copy link
Copy Markdown

⚡ Benchmark Results

PR e65b46a main 8f65994
Config 50 connections, 10s/test 50 connections, 10s/test
Benchmark Req/s vs main Avg Latency vs main P99 Latency Throughput
minimal 1341 🟢 +14.2% 36.67 ms 🟢 -12.7% 77 ms 0.9 MB/s
small 1300 🟢 +1.9% 37.77 ms 🟢 -2.3% 68 ms 1.3 MB/s
medium 411 🟢 +7.0% 120.71 ms 🟢 -6.7% 168 ms 6.1 MB/s
large 49 🟢 +3.0% 969.9 ms 🟢 -3.1% 1514 ms 4.9 MB/s
deep 839 🔴 -2.9% 58.86 ms 🔴 +3.1% 103 ms 2.9 MB/s
wide 71 🟢 +2.7% 668.21 ms 🟢 -2.8% 1115 ms 3.9 MB/s
cached 3464 🟢 +13.9% 13.83 ms 🟢 -13.3% 27 ms 51.0 MB/s
client-min 481 🟢 +8.5% 102.86 ms 🟢 -7.9% 166 ms 2.1 MB/s
client-small 492 🟢 +6.9% 100.47 ms 🟢 -6.8% 158 ms 2.3 MB/s
client-med 365 🟢 +7.5% 135.67 ms 🟢 -6.9% 190 ms 6.7 MB/s
client-large 84 ⚪ +0.2% 577.29 ms 🟢 -1.1% 1107 ms 8.8 MB/s
client-deep 460 🟢 +5.6% 107.29 ms 🟢 -5.2% 158 ms 3.3 MB/s
client-wide 145 🟢 +5.1% 338.57 ms 🟢 -3.8% 601 ms 8.5 MB/s
rsc-client-large 1105 🟢 +3.4% 44.6 ms 🟢 -3.5% 66 ms 2.9 MB/s
rsc-client-wide 1118 🟢 +4.1% 44.05 ms 🟢 -4.2% 63 ms 2.9 MB/s
static-json 7514 🟢 +12.2% 6.18 ms 🟢 -11.3% 16 ms 3.1 MB/s
static-js 7250 🟢 +10.0% 6.33 ms 🟢 -11.3% 16 ms 4.3 MB/s
404-miss 4928 🟢 +3.2% 9.5 ms 🟢 -3.5% 22 ms 0.6 MB/s
hybrid-min 472 🟢 +6.2% 104.99 ms 🟢 -5.5% 162 ms 2.3 MB/s
hybrid-small 459 🟢 +7.0% 107.93 ms 🟢 -6.3% 164 ms 2.7 MB/s
hybrid-medium 245 🟢 +8.0% 202.14 ms 🟢 -7.6% 293 ms 10.4 MB/s
hybrid-large 42 🟢 +5.0% 1080.2 ms 🟢 -8.0% 1851 ms 13.5 MB/s
hybrid-deep 375 🟢 +5.7% 131.24 ms 🟢 -6.0% 184 ms 5.2 MB/s
hybrid-wide 61 🔴 -1.9% 769.31 ms 🟢 -3.4% 1312 ms 12.0 MB/s
hybrid-cached 2957 🟢 +8.1% 16.43 ms 🟢 -7.6% 30 ms 125.9 MB/s
hybrid-client-min 495 🟢 +5.7% 99.53 ms 🟢 -6.3% 152 ms 2.2 MB/s
hybrid-client-small 487 🟢 +5.8% 101.83 ms 🟢 -5.3% 153 ms 2.4 MB/s
hybrid-client-medium 361 🟢 +4.8% 137.15 ms 🟢 -4.3% 199 ms 6.7 MB/s
hybrid-client-large 83 🟢 +4.7% 585.15 ms 🟢 -3.6% 1112 ms 8.7 MB/s
hybrid-client-deep 457 🟢 +8.8% 108.28 ms 🟢 -7.9% 157 ms 3.3 MB/s
hybrid-client-wide 147 🟢 +13.1% 336.68 ms 🟢 -11.0% 585 ms 8.6 MB/s
Legend

🟢 > 1% improvement | 🔴 > 1% regression | ⚪ within noise margin

Benchmarks run on GitHub Actions runners (shared infrastructure) — expect ~5% variance between runs. Consistent directional changes across multiple routes are more meaningful than any single number.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 27, 2026

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
953 3 950 16
View the top 3 failed test(s) by shortest run time
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: get-started-ts > starts in production mode
Stack Traces | 0.000581s run time
AssertionError: production start should work: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:74:66
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: router > starts in production mode
Stack Traces | 0.000886s run time
AssertionError: production start should work: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:74:66
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: get-started > starts in production mode
Stack Traces | 0.000896s run time
AssertionError: production start should work: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:74:66
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: get-started-ts > builds the app
Stack Traces | 0.000985s run time
AssertionError: build should succeed: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:70:58
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: router > builds the app
Stack Traces | 0.00113s run time
AssertionError: build should succeed: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:70:58
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: get-started > builds the app
Stack Traces | 0.00118s run time
AssertionError: build should succeed: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:70:58
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: get-started-ts > dev mode starts and serves the app
Stack Traces | 0.00598s run time
AssertionError: dev mode should work: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:66:56
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: get-started > dev mode starts and serves the app
Stack Traces | 0.0081s run time
AssertionError: dev mode should work: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:66:56
__test__/deno.spec.mjs > create-react-server: deno runtime (pnpm) > preset: router > dev mode starts and serves the app
Stack Traces | 0.00849s run time
AssertionError: dev mode should work: expected false to be true // Object.is equality

- Expected
+ Received

- true
+ false

 ❯ __test__/deno.spec.mjs:66:56

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

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.

2 participants