ADR 0125 — Streaming bodies bound retained bytes, not lifetime traffic
Context
A finite HTTP response needs a cumulative byte ceiling: otherwise a caller that buffers it can grow without bound. A long-lived NDJSON or SSE subscription has a different invariant. Its frame, socket queue and consumer backlog must be bounded, but the total bytes successfully drained over its lifetime are not retained memory. Applying the unary 16 MiB total to both shapes eventually terminates every healthy subscription.
Neither Content-Type nor an operation name declares memory ownership. Infinity
also erases the distinction instead of expressing it, and an automatic reconnect
would invent delivery semantics the transport cannot know.
Decision
createUnixClientTransport has two explicit response-body modes:
boundedis the default and appliesmaxResponseBytes, defaulting to 16 MiB;streamingremoves only the cumulative response-total check and cannot be combined withmaxResponseBytes.
Streaming remains pull-driven. Node pauses its IncomingMessage behind a
one-chunk Web stream queue; Bun pauses its socket and emits at most 64 KiB from
the owned wire buffer per pull. Request bytes, headers, connections, redirects,
framing validation, cancellation and transport shutdown keep their finite bounds.
Frame and application backlog bounds belong to the stream contract and consumer.
Consequences
- Unary clients stay fail-closed without configuration changes.
- A subscription opts in at composition time with
responseBodyMode: 'streaming'; the adapter never guesses from response data. - Lifetime traffic may exceed 16 MiB while a stalled reader still stops the producer and cancellation releases the connection slot.
- Applications needing unary and streaming calls can compose separate transport handles, making the weaker cumulative policy visible at the call boundary.