HAMZA KAREEM / CONCEPTS hamzawi.dev

Lesson 362 min read

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.

  1. Why is it lopsided?
  2. Name two fixes — one at L7, one without changing your LB layer.