react-server675fbba4
react-servercommit84abefcfbbb1

feat: backpressure (#408)

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.

Author
Viktor Lázár <lazarv1982@gmail.com>
Date
Commit
84abefcfbbb163d8d1391d5ef5a5af3f2dc8838f
26 files changed+2055 -125