Every request returns 200 OK. The transport fails 4,800 times.
This interactive lab models an HTTP/1.1 idle-timeout mismatch. An upstream shortens its timeout from 60 seconds to 30 seconds while a client pool still keeps sockets for 60 seconds. Hot and warm routes stay inside the new boundary; a sparse route wakes after 45 seconds and finds that the other side already discarded its connection.
The deterministic simulation and HTTP service are executable CoffeeScript. A dependency-free browser workbench compares the hidden cost of retries with three lifetime policies.
The model runs 500 idempotent GETs per second for 600 seconds. At second 120, the upstream timeout changes from 60 seconds to 30 seconds. Two percent of traffic belongs to a sparse 45-second connection cohort.
| Strategy | HTTP success | Transport success | Resets | Connection opens | Sparse p99 | Background probes |
|---|---|---|---|---|---|---|
| Retry mask | 100% | 98.43% | 4,800 | 4,800 | 49 ms | 0 |
| Fleet-wide short TTL | 100% | 100% | 0 | 48,000 | 32 ms | 0 |
| Heartbeat every socket | 100% | 100% | 0 | 0 | 8 ms | 30,000 |
| Route-aware lifetime | 100% | 100% | 0 | 4,800 | 32 ms | 0 |
The logical-request metric says all four strategies are identical. The attempt ledger shows very different operational costs:
- Retry mask: preserves every response, but sparse requests pay reset detection, retry backoff, and a new handshake.
- Fleet-wide short TTL: prevents stale reuse by churning healthy warm connections too.
- Heartbeat: removes request-path failures at the cost of continual background traffic across the idle fleet.
- Route-aware lifetime: applies early eviction only where idle age crosses the new upstream boundary.
An HTTP status describes the logical request. ECONNRESET belongs to one
attempt on one persistent connection. The causal record is:
trace_id
+ request_attempt
+ connection_id
+ connection_age_ms
+ idle_age_ms
+ client_idle_timeout_ms
+ upstream_idle_timeout_ms
+ socket_error
Keeping those fields together lets telemetry.sh expose a retry that otherwise looks like ordinary latency, isolate the sparse route, identify the timeout boundary, and compare request-path recovery with connection churn or heartbeat traffic.
This differs from connection-pool saturation: the pool has enough capacity. The failure is disagreement about how long an idle socket remains reusable.
Requirements: Node.js 20 or newer and npm.
npm install
make runOpen http://localhost:8080.
Run the CoffeeScript model directly:
npm run modelOverride model inputs through environment variables:
REQUESTS_PER_SECOND=1000 \
SPARSE_ROUTE_PERCENT=5 \
UPSTREAM_TIMEOUT_AFTER_SECONDS=20 \
SPARSE_GAP_SECONDS=35 \
npm run modelOr use the container:
docker build -t keepalive-mismatch-lab .
docker run --rm -p 8080:8080 keepalive-mismatch-labThe checks cover deterministic results, boundary and relational clamping, safe and ineffective heartbeat policies, CoffeeScript execution, JavaScript syntax, invalid HTTP input, security headers, the API contract, and responsive page structure.
npm install
make check
make container-testGitHub Actions runs both the native and production-container suites.
RFC 9112, section 9.5 explains that persistent-connection timeout lengths are not prescribed and that a client, server, or proxy may close the transport at any time—including while one peer considers it idle and the other has begun a request. Its retry section says implementations need to anticipate asynchronous closes.
RFC 9110, section 9.2.2 defines idempotent methods and permits automatic recovery after a connection failure. It cautions against automatically retrying non-idempotent methods unless the client can establish idempotent semantics or know the original request was not applied. This lab therefore models GET requests only.
This is a deterministic teaching model, not a network benchmark. Route gaps represent connection cohorts rather than a specific HTTP client implementation. Real close detection, pool eviction, TCP behavior, proxy configuration, request replay, and handshake costs vary.
The lab opens no workload sockets; it safely models the lifecycle and telemetry curves instead of disrupting the host network.
Built to demonstrate why request and transport evidence belong together with telemetry.sh.