L4 vs L7 Load Balancing
Cloud infrastructurecloud infrastructure / networking
Both spread traffic across servers. But they operate at different layers — and picking the wrong one quietly costs you features or latency.
The core difference
- L4 (transport): routes by IP + port. It sees a TCP/UDP stream, not what's inside. Forwards packets blind.
- L7 (application): terminates the connection, reads the HTTP request, routes by path, header, cookie, hostname.
Think of it like mail: L4 sorts by ZIP code. L7 opens the envelope and reads the letter.
Side by side
L4 L7
Routes on IP:port URL / header / cookie
Sees payload No Yes (decrypts TLS)
Latency Lower Slightly higher
Cost/CPU Cheap More (parses + re-encrypts)
Path routing ❌ ✅ /api → svc-a, /img → svc-b
Sticky sessions By source IP By cookie (accurate)
TLS Passthrough Terminates (offload)
When to use which
- L7: microservices behind one domain, canary by header, A/B by cookie, WAF, request-level metrics. This is your default for web apps. (nginx, Envoy, ALB, HAProxy in http mode.)
- L4: raw TCP/UDP (databases, gRPC streams, game servers, SMTP), millions of connections where you can't afford to parse each request, or end-to-end TLS you refuse to terminate. (AWS NLB, HAProxy tcp mode, IPVS.)
The gotcha that trips people up
With L4, every request on one TCP connection goes to the SAME backend — the LB never re-reads. So HTTP/2 or gRPC, which multiplexes many requests over ONE long-lived connection, will pin all of them to a single pod. Your fancy autoscaler adds pods; traffic stays lopsided because L4 can't see the individual streams.
Fix: use an L7 balancer that understands HTTP/2, or enable per-request load balancing at the app/mesh layer (e.g. Envoy, a service mesh sidecar). This is exactly why gRPC-in-Kubernetes tutorials warn "the default Service load balances connections, not requests."
Another trap: L4 sticky sessions key on source IP. Put a corporate NAT or mobile carrier in front and thousands of users share one IP → they all land on one server. L7 cookie-based stickiness avoids this.
Quick mental test
Ask: "Do I need to route based on what's INSIDE the request?"
- Yes (path, header, cookie, host) → L7
- No, just spread connections fast → L4
Your exercise
You run gRPC between two services in Kubernetes using a plain ClusterIP Service. Traffic to one replica is 5x the others despite equal CPU limits.
- Why is it lopsided?
- Name two fixes — one at L7, one without changing your LB layer.