You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The name is a one-way door and was decided deliberately.max_connections was rejected: Go can only
enforce per host per transport, and [rate_limit] max_conns already denotes an inbound per-listener
cap. Two similar names pointing in opposite directions is the trap ADR 0016 avoided by keeping tls and backend_tls distinct.
Deliverables
max_connections_per_backend → http.Transport.MaxConnsPerHost — the only lever Go offers that
bounds sockets without defeating pooling and that honours the request context while queueing for a dial.
Not applicable to native gRPC or transcoding, where one HTTP/2 connection carries all streams;
validation warns when set on such a route.
Health-check clients are exempt, so a saturated pool can still observe recovery.
Idle connections count toward the limit until IdleConnTimeout.
Native gRPC and transcoding admission — internal/handler/grpcproxy.go and internal/transcode
have their own handler entry points and each needs the Admit call from 141-A.
Lifetime accounting verified across protocols, using the existing release discipline rather than
new mechanisms.
Accounting matrix (normative)
Client protocol
A_p / A_b
Backend C_b
Which limit binds
HTTP/1.1, no keep-alive
+1 per request
+1 per request
both, in lockstep
HTTP/1.1 keep-alive
+1 per request
conns reused; idle conns count until IdleConnTimeout
either; C_b can bind while A_b is low
HTTP/2
+1 per stream
typically 1
request limit; connection limit rarely binds
HTTP/3
+1 per stream
unaffected — the backend leg is still HTTP/1.1 or h2
request limit only
WebSocket / 101
+1 until releaseRWBody.Close()
+1 dedicated, hijacked, never pooled
both
Server-sent events
+1 for the response lifetime
as HTTP/1.1
request limit
gRPC unary
+1 per call
shared h2 conn
request limit
gRPC server/client/bidi streaming
+1 for the whole stream lifetime
shared
request limit
gRPC transcoding, unary and streaming
+1 per call/stream
shared
request limit
HTTP/3 requires no protocol-specific code. Jul's HTTP/3 support is inbound only — internal/server/http3.go
runs a quic-go http3.Server, and there is no HTTP/3 backend transport. HTTP/3 requests are served by the same handler tree via acquireGen() and upstream.WithSnapshot, so admission applies unchanged. h3Conn.Close(ctx) does GOAWAY and drain bounded by shutdown_timeout, so a long-lived h3 stream holds
its slot and its handlerGen reference exactly as an h2 stream does. WebSocket over HTTP/3 is not
applicable — h3 uses extended CONNECT, which Jul does not implement.
The operator-facing sentence for the docs: max_active_requests counts requests and streams; max_connections_per_backend counts sockets. Under HTTP/2, HTTP/3 and gRPC one socket carries many
streams, so the request limit normally binds first; under HTTP/1.1 and WebSocket the two move together.
Acceptance criteria
MaxConnsPerHost wired from the resolved policy; warns on gRPC and transcoding routes; health-check
clients exempt.
Native gRPC and transcoding acquire and release admission on every terminal path, including
half-open and cancelled streams.
Every row of the accounting matrix is asserted by a test.
A long-lived stream holds its slot for its full lifetime and releases exactly once.
Lifecycle registry entry for max_connections_per_backend, docs for the accounting matrix.
Required tests
Per-protocol accounting: HTTP/1.1 keep-alive, HTTP/2 multi-stream, HTTP/3, WebSocket lifetime, SSE,
gRPC unary and all three streaming shapes, transcoding.
Client disconnect and panic release the slot on each protocol.
A MaxConnsPerHost integration test gating the documented semantics, covering interaction with MaxIdleConnsPerHost: 32 under keep-alive churn. The stdlib interaction is asserted from documentation
today, not measured — if it degrades pooling, the field's meaning changes and the name is already fixed.
Non-goals for this slice
FastCGI/uWSGI (141-C); L4 (141-D); cross-protocol soak and BenchmarkAdmit_* (141-E).
Note
Sub-issue of #141. Authority:
ADR 0017
(#116).
Depends on: #283 (141-A) only. Independent of 141-C and 141-D — these three may proceed in parallel.
Scope
Physical connection bounds, plus admission integration and lifetime accounting for every remaining
in-tree protocol.
Public configuration
Stateless, so settable at pool or location level (location wins) — transports are already built per
location by
newProxyTransport(loc, policy).The name is a one-way door and was decided deliberately.
max_connectionswas rejected: Go can onlyenforce per host per transport, and
[rate_limit] max_connsalready denotes an inbound per-listenercap. Two similar names pointing in opposite directions is the trap ADR 0016 avoided by keeping
tlsandbackend_tlsdistinct.Deliverables
max_connections_per_backend→http.Transport.MaxConnsPerHost— the only lever Go offers thatbounds sockets without defeating pooling and that honours the request context while queueing for a dial.
validation warns when set on such a route.
IdleConnTimeout.internal/handler/grpcproxy.goandinternal/transcodehave their own handler entry points and each needs the
Admitcall from 141-A.new mechanisms.
Accounting matrix (normative)
A_p/A_bC_bIdleConnTimeoutC_bcan bind whileA_bis lowreleaseRWBody.Close()HTTP/3 requires no protocol-specific code. Jul's HTTP/3 support is inbound only —
internal/server/http3.goruns a quic-go
http3.Server, and there is no HTTP/3 backend transport. HTTP/3 requests are served by thesame handler tree via
acquireGen()andupstream.WithSnapshot, so admission applies unchanged.h3Conn.Close(ctx)does GOAWAY and drain bounded byshutdown_timeout, so a long-lived h3 stream holdsits slot and its
handlerGenreference exactly as an h2 stream does. WebSocket over HTTP/3 is notapplicable — h3 uses extended CONNECT, which Jul does not implement.
The operator-facing sentence for the docs:
max_active_requestscounts requests and streams;max_connections_per_backendcounts sockets. Under HTTP/2, HTTP/3 and gRPC one socket carries manystreams, so the request limit normally binds first; under HTTP/1.1 and WebSocket the two move together.
Acceptance criteria
MaxConnsPerHostwired from the resolved policy; warns on gRPC and transcoding routes; health-checkclients exempt.
half-open and cancelled streams.
max_connections_per_backend, docs for the accounting matrix.Required tests
gRPC unary and all three streaming shapes, transcoding.
MaxConnsPerHostintegration test gating the documented semantics, covering interaction withMaxIdleConnsPerHost: 32under keep-alive churn. The stdlib interaction is asserted from documentationtoday, not measured — if it degrades pooling, the field's meaning changes and the name is already fixed.
Non-goals for this slice
FastCGI/uWSGI (141-C); L4 (141-D); cross-protocol soak and
BenchmarkAdmit_*(141-E).Completion evidence