Load Balancing And Routing
Purpose
Choose how a request reaches a replica, and know what that choice can and cannot express. The
decision is not merely "L4 or L7" as layer numbers; it is capability. An L7 intermediary can
parse application messages and may route HTTP requests/RPC streams, apply policy and expose
application telemetry. An L4 data plane generally selects by transport flow/connection and
cannot safely infer HTTP semantics. Capability still depends on protocol and configuration: a
long-lived streaming RPC remains one routed stream even through an L7 proxy, and retries are
legal only under the operation's deadline/idempotency contract.
The failure this prevents is the fleet that is balanced on paper and skewed in production. An
L4 balancer plus long-lived HTTP/2 or gRPC connections balances connections, and a client
that opens one connection and multiplexes ten thousand requests over it sends every one of
them to a single replica. Adding replicas does not help; the connection does not move. Nothing
is unhealthy, no error is logged, and the only visible symptom is that per-replica request
rate is uneven while connection counts are not.
Workflow
- Name the routing unit and information available. TCP flow, HTTP request, RPC stream,
session, tenant or key lead to different behavior. Header/path routing and semantic retries
require application parsing; ownership routing may require a key-aware client or directory.
- Check the connection lifetime against the protocol. HTTP/1.1 with keep-alive, HTTP/2
and gRPC all hold connections open. Sequential HTTP/1.1 reuse also pins repeated work;
HTTP/2 multiplexing can amplify it. See
references/connection-lifetime-and-l4.md.
- Measure offered work, admitted work and cost before choosing an algorithm. Normalize
per-endpoint requests, active streams, bytes, CPU/service time, queueing and capacity.
Connection counts alone do not mean equal load because connections carry different work.
- Pick the algorithm by the property it optimises. Round-robin ignores request cost;
least-request adapts to it; power-of-two-choices is the distributed approximation of
least-loaded. See
references/routing-modes.md.
- Design active readiness and passive outlier detection as complementary signals. State
thresholds, recovery, locality and correlated-failure behavior. Cap ejection/admission so
removing hosts cannot overload the survivors; choose fail-open versus fail-closed by the
safety contract, not as a universal panic rule.
- Sequence the drain. Mark terminating/not-ready, allow routing state to converge, stop
admitting new application work while keeping transports alive for in-flight work, send
GOAWAY where applicable, and bound completion.
preStop sleep is one coarse mechanism;
endpoint/LB draining behavior must be verified. Budget arithmetic is
kubernetes-service-lifecycle.
- Verify with a rollout, not a review. Run an open-loop client through a deploy and a
scale-up, recording HTTP errors, gRPC terminal statuses, resets/timeouts and per-replica
capacity-normalized work share. HTTP 200 alone does not establish RPC success.
Decision block
Use an L4 balancer when:
- the protocol is not HTTP (raw TCP, a database proxy), or connections are short-lived and
numerous enough that connection balancing approximates request balancing
- per-request routing, retries and traffic splitting are genuinely not required
Avoid an L4 balancer when:
- traffic is HTTP/2 or gRPC over long-lived connections — it will balance connections and
pin request load to whichever replicas the clients happen to hold
Use an L7 proxy when:
- you need per-request balancing, header- or path-based routing, weighted rollout, retries,
or per-request observability; account for whether it adds a hop, TLS boundary, CPU and
another failure/queueing domain
Prefer client-side balancing when:
- callers are few, internal, and share a language or mesh runtime; the extra hop's latency
matters; and you can distribute discovery and policy to every client
Avoid client-side balancing when:
- clients are third-party or polyglot, or a policy change would require redeploying every
caller — the policy is then as hard to change as the clients
Prefer routing by key (sharding-and-partitioning) instead when:
- a request must reach the one replica that owns its key. That is placement, not balancing,
and a least-request policy actively breaks it
Rules
- Inspect deployed JDK, grpc-java transport/resolver, proxy and Kubernetes versions before
applying API or policy guidance. The Java example is partial; no stack upgrade is implied.
- State the balancer's unit of work. L4 usually balances transport flows; L7 can balance
requests or streams. Verify actual connection pooling and upstream routing rather than
inferring it from a product label.
- Kubernetes
Service / ClusterIP exposes an L4 virtual service whose implementation may be
kube-proxy (iptables/IPVS/nftables), Windows networking or eBPF. It selects new
connections. gRPC or HTTP/2 traffic through a ClusterIP therefore pins: the fix is an L7
proxy in the path, or a headless Service plus client-side balancing — not a different
sessionAffinity setting.
- The observable signature of the multiplexing problem: per-pod
rate(http_server_requests_seconds_count[5m]) (or the gRPC equivalent) varies by multiples
across pods, while per-pod established-connection counts are within a few of each other. A
newly scaled-up pod that stays near zero request rate is the same symptom.
- Connection recycling can bound stale placement from the server, client library or proxy.
Graceful HTTP/2 GOAWAY plus jitter avoids synchronized reconnects, but recycling is a coarse
mitigation and can increase handshake/TLS/connection pressure. Ensure clients re-resolve
and retry only safe streams.
- Round-robin distributes configured routing units (requests at L7, flows at L4), not
work. With heterogeneous request cost it
can produce even routing-unit counts and uneven latency; that is not a broken balancer, it is the
wrong metric being equalised.
- Least-request and least-connections are not synonyms under multiplexing. Least-request can
react to outstanding request count; that count still misses heterogeneous cost and can bias
toward a freshly started/cold endpoint. Select weighted least-request, EWMA latency,
power-of-two choices or round-robin from measured workload and locality constraints.
- Distributed least-loaded can herd when balancers share stale load information and choose
the same endpoint. Independent random candidate selection can reduce that correlation,
but power-of-two choices is not universally superior: weights, locality, signal quality
and the freshness/cost of global coordination determine the comparison.
- A health check is a timeout-based observation, not ground truth. Aggressive thresholds turn a
shared-dependency blip into a fleet-wide ejection: every replica fails at once, the balancer
ejects them all, and there is no backend left. Cap ejection at a fraction of the upstream
(a max-ejection-percentage) and reserve enough capacity. A fail-open panic mode may preserve
degraded availability, while authentication/corruption hazards may require fail-closed.
Correlated failure is
failure-models.
- Coordinate readiness and balancer checks, but do not assume they are duplicates. Readiness
expresses endpoint lifecycle/local ability; passive ejection sees path- and request-specific
failures. Document precedence and recovery so disagreement is diagnosable.
- Draining is a sequence with overlapping control/data planes: stop advertising, wait for
bounded propagation, reject/redirect new work, finish or terminate in-flight work, then
close. A fixed
preStop sleep may cover propagation but does not prove it; measure new
arrivals and active streams and configure LB-specific deregistration behavior —
kubernetes-service-lifecycle owns the budget.
- Session affinity is a routing mode here, not a session design. Affinity ends whenever the
replica or the affinity table changes; whether that is acceptable is
stateless-service-design.
- Balancing does not create capacity. When every replica is saturated, spreading the load
evenly only spreads the failure evenly; the answer is
rate-limiting-and-load-shedding.
Routing invariants and security
- Preserve end-to-end client identity and trusted forwarding headers through explicit proxy
trust configuration; never authorize from an untrusted
X-Forwarded-For.
- Keep tenant/session/key affinity scoped and bounded. A large tenant can remain hot even when
request counts are balanced; isolate or shard it rather than hiding skew with stickiness.
- Retry only before response commitment and only for operations whose ambiguity/idempotency
rules allow it. Enforce one end-to-end attempt budget to avoid multiplicative proxy/client
retries.
- During weighted rollout, measure request and work share, success, latency and state/schema
compatibility. Connection/stream lifetime can make configured weights differ from observed
traffic for a long time.
References
- Connection lifetime and why L4 pins load — how
HTTP/2 and gRPC multiplexing defeats connection balancing, the metric comparison that
investigates it, and the four fixes with their costs including the Java client and server
settings that matter. Read when request rate is skewed across replicas, when a scaled-up
pod stays idle, or before putting gRPC behind an L4 hop.
- Routing modes — the algorithms compared by the property each
optimises, health-check and outlier-ejection settings with the fleet-ejection hazard, the
drain sequence, and a decision table across L4, L7 and client-side. Read when configuring a
balancer or a mesh, or when a dependency blip ejected more hosts than it should have.
1---2name: load-balancing-and-routing3description: Getting a request to a replica that can serve it: L4 versus L7 by capability rather than layer number, why an L4 balancer in front of long-lived HTTP/2 or gRPC connections balances connections instead of requests and pins a client to one replica, the balancing algorithms and what each optimises, power-of-two-choices, health checking and outlier ejection with the fleet-ejection hazard, and connection draining. Use when per-pod request rate is skewed while connection counts look even, when one replica is hot after a scale-up, when gRPC or HTTP/2 crosses a ClusterIP Service, when a dependency blip ejects the whole upstream, or when choosing between an ingress proxy, a mesh and client-side balancing. Does not cover why a replica is interchangeable (stateless-service-design), the in-pod proxy form (ambassador-pattern), what to do when every replica is busy (rate-limiting-and-load-shedding), readiness and drain mechanics (kubernetes-service-lifecycle), or routing a key to its owner (sharding-and-partitioning).4---56# Load Balancing And Routing78## Purpose910Choose how a request reaches a replica, and know what that choice can and cannot express. The11decision is not merely "L4 or L7" as layer numbers; it is capability. An L7 intermediary can12parse application messages and may route HTTP requests/RPC streams, apply policy and expose13application telemetry. An L4 data plane generally selects by transport flow/connection and14cannot safely infer HTTP semantics. Capability still depends on protocol and configuration: a15long-lived streaming RPC remains one routed stream even through an L7 proxy, and retries are16legal only under the operation's deadline/idempotency contract.1718The failure this prevents is the fleet that is balanced on paper and skewed in production. An19L4 balancer plus long-lived HTTP/2 or gRPC connections balances _connections_, and a client20that opens one connection and multiplexes ten thousand requests over it sends every one of21them to a single replica. Adding replicas does not help; the connection does not move. Nothing22is unhealthy, no error is logged, and the only visible symptom is that per-replica request23rate is uneven while connection counts are not.2425## Workflow26271. **Name the routing unit and information available.** TCP flow, HTTP request, RPC stream,28 session, tenant or key lead to different behavior. Header/path routing and semantic retries29 require application parsing; ownership routing may require a key-aware client or directory.302. **Check the connection lifetime against the protocol.** HTTP/1.1 with keep-alive, HTTP/231 and gRPC all hold connections open. Sequential HTTP/1.1 reuse also pins repeated work;32 HTTP/2 multiplexing can amplify it. See `references/connection-lifetime-and-l4.md`.333. **Measure offered work, admitted work and cost before choosing an algorithm.** Normalize34 per-endpoint requests, active streams, bytes, CPU/service time, queueing and capacity.35 Connection counts alone do not mean equal load because connections carry different work.364. **Pick the algorithm by the property it optimises.** Round-robin ignores request cost;37 least-request adapts to it; power-of-two-choices is the distributed approximation of38 least-loaded. See `references/routing-modes.md`.395. **Design active readiness and passive outlier detection as complementary signals.** State40 thresholds, recovery, locality and correlated-failure behavior. Cap ejection/admission so41 removing hosts cannot overload the survivors; choose fail-open versus fail-closed by the42 safety contract, not as a universal panic rule.436. **Sequence the drain.** Mark terminating/not-ready, allow routing state to converge, stop44 admitting new application work while keeping transports alive for in-flight work, send45 GOAWAY where applicable, and bound completion. `preStop` sleep is one coarse mechanism;46 endpoint/LB draining behavior must be verified. Budget arithmetic is47 `kubernetes-service-lifecycle`.487. **Verify with a rollout, not a review.** Run an open-loop client through a deploy and a49 scale-up, recording HTTP errors, gRPC terminal statuses, resets/timeouts and per-replica50 capacity-normalized work share. HTTP 200 alone does not establish RPC success.5152## Decision block5354```text55Use an L4 balancer when:56- the protocol is not HTTP (raw TCP, a database proxy), or connections are short-lived and57 numerous enough that connection balancing approximates request balancing58- per-request routing, retries and traffic splitting are genuinely not required59Avoid an L4 balancer when:60- traffic is HTTP/2 or gRPC over long-lived connections — it will balance connections and61 pin request load to whichever replicas the clients happen to hold62Use an L7 proxy when:63- you need per-request balancing, header- or path-based routing, weighted rollout, retries,64 or per-request observability; account for whether it adds a hop, TLS boundary, CPU and65 another failure/queueing domain66Prefer client-side balancing when:67- callers are few, internal, and share a language or mesh runtime; the extra hop's latency68 matters; and you can distribute discovery and policy to every client69Avoid client-side balancing when:70- clients are third-party or polyglot, or a policy change would require redeploying every71 caller — the policy is then as hard to change as the clients72Prefer routing by key (sharding-and-partitioning) instead when:73- a request must reach the one replica that owns its key. That is placement, not balancing,74 and a least-request policy actively breaks it75```7677## Rules7879- Inspect deployed JDK, grpc-java transport/resolver, proxy and Kubernetes versions before80 applying API or policy guidance. The Java example is partial; no stack upgrade is implied.81- State the balancer's unit of work. L4 usually balances transport flows; L7 can balance82 requests or streams. Verify actual connection pooling and upstream routing rather than83 inferring it from a product label.84- Kubernetes `Service` / `ClusterIP` exposes an L4 virtual service whose implementation may be85 kube-proxy (iptables/IPVS/nftables), Windows networking or eBPF. It selects new86 connections. gRPC or HTTP/2 traffic through a ClusterIP therefore pins: the fix is an L787 proxy in the path, or a headless Service plus client-side balancing — not a different88 `sessionAffinity` setting.89- The observable signature of the multiplexing problem: per-pod90 `rate(http_server_requests_seconds_count[5m])` (or the gRPC equivalent) varies by multiples91 across pods, while per-pod established-connection counts are within a few of each other. A92 newly scaled-up pod that stays near zero request rate is the same symptom.93- Connection recycling can bound stale placement from the server, client library or proxy.94 Graceful HTTP/2 GOAWAY plus jitter avoids synchronized reconnects, but recycling is a coarse95 mitigation and can increase handshake/TLS/connection pressure. Ensure clients re-resolve96 and retry only safe streams.97- Round-robin distributes configured routing units (requests at L7, flows at L4), not98 **work**. With heterogeneous request cost it99 can produce even routing-unit counts and uneven latency; that is not a broken balancer, it is the100 wrong metric being equalised.101- Least-request and least-connections are not synonyms under multiplexing. Least-request can102 react to outstanding request count; that count still misses heterogeneous cost and can bias103 toward a freshly started/cold endpoint. Select weighted least-request, EWMA latency,104 power-of-two choices or round-robin from measured workload and locality constraints.105- Distributed least-loaded can herd when balancers share stale load information and choose106 the same endpoint. Independent random candidate selection can reduce that correlation,107 but power-of-two choices is not universally superior: weights, locality, signal quality108 and the freshness/cost of global coordination determine the comparison.109- **A health check is a timeout-based observation, not ground truth.** Aggressive thresholds turn a110 shared-dependency blip into a fleet-wide ejection: every replica fails at once, the balancer111 ejects them all, and there is no backend left. Cap ejection at a fraction of the upstream112 (a max-ejection-percentage) and reserve enough capacity. A fail-open panic mode may preserve113 degraded availability, while authentication/corruption hazards may require fail-closed.114 Correlated failure is `failure-models`.115- Coordinate readiness and balancer checks, but do not assume they are duplicates. Readiness116 expresses endpoint lifecycle/local ability; passive ejection sees path- and request-specific117 failures. Document precedence and recovery so disagreement is diagnosable.118- Draining is a sequence with overlapping control/data planes: stop advertising, wait for119 bounded propagation, reject/redirect new work, finish or terminate in-flight work, then120 close. A fixed `preStop` sleep may cover propagation but does not prove it; measure new121 arrivals and active streams and configure LB-specific deregistration behavior —122 `kubernetes-service-lifecycle` owns the budget.123- Session affinity is a routing mode here, not a session design. Affinity ends whenever the124 replica or the affinity table changes; whether that is acceptable is125 `stateless-service-design`.126- Balancing does not create capacity. When every replica is saturated, spreading the load127 evenly only spreads the failure evenly; the answer is `rate-limiting-and-load-shedding`.128129## Routing invariants and security130131- Preserve end-to-end client identity and trusted forwarding headers through explicit proxy132 trust configuration; never authorize from an untrusted `X-Forwarded-For`.133- Keep tenant/session/key affinity scoped and bounded. A large tenant can remain hot even when134 request counts are balanced; isolate or shard it rather than hiding skew with stickiness.135- Retry only before response commitment and only for operations whose ambiguity/idempotency136 rules allow it. Enforce one end-to-end attempt budget to avoid multiplicative proxy/client137 retries.138- During weighted rollout, measure request and **work** share, success, latency and state/schema139 compatibility. Connection/stream lifetime can make configured weights differ from observed140 traffic for a long time.141142## References143144- [Connection lifetime and why L4 pins load](references/connection-lifetime-and-l4.md) — how145 HTTP/2 and gRPC multiplexing defeats connection balancing, the metric comparison that146 investigates it, and the four fixes with their costs including the Java client and server147 settings that matter. Read when request rate is skewed across replicas, when a scaled-up148 pod stays idle, or before putting gRPC behind an L4 hop.149- [Routing modes](references/routing-modes.md) — the algorithms compared by the property each150 optimises, health-check and outlier-ejection settings with the fleet-ejection hazard, the151 drain sequence, and a decision table across L4, L7 and client-side. Read when configuring a152 balancer or a mesh, or when a dependency blip ejected more hosts than it should have.