← Computer Networks
Traffic Management & Resilience

Load Balancing — L4 vs L7

Watch: "Load balancing in Layer 4 vs Layer 7 with HAPROXY Examples" — Hussein Nasser

Every large system eventually needs to spread traffic across many backend instances, and the level at which you do that — transport vs application — determines how smart, how fast, and how expensive the balancing is. Picture 100,000 requests/sec hitting a product that one server simply cannot absorb: you add instances, and now something has to decide which instance gets each request. This is one of the most common whiteboard questions in backend and infra interviews, and interviewers dig into the mechanics, not just the definitions.

What Is L4 Load Balancing?

L4 (Layer 4, "transport layer") load balancing spreads traffic based only on connection-level information — the source and destination IP address and port — without ever looking at what's actually inside the request. It's fast and cheap because it's just forwarding packets, the same way a mail sorting facility can route a sealed envelope by the address on the outside without opening it.

What Is L7 Load Balancing?

L7 (Layer 7, "application layer") load balancing actually opens up each request — reading the URL, headers, and cookies — before deciding where to send it. That lets it make smarter decisions, like sending everyone hitting /checkout to one group of servers and everyone hitting /search to another, at the cost of extra processing work per request.

Where This Is Used

A Day-to-Day Example

Picture a busy restaurant with a host stand out front. An L4 approach is like a host who only glances at how many people are in each party and sends them to whichever open table fits, without asking what they want to eat. An L7 approach is like a host who actually listens to the request — "we're here for the tasting menu" — and seats that party in the section staffed by the chef who handles it, because they took the time to understand what was being asked for.

Deep Dive: What L4 vs L7 Actually Means on the Wire

"Layer 4" and "Layer 7" refer to the OSI model. An L4 (transport-layer) balancer never assembles or reads the HTTP request — it looks at the TCP/UDP header (source IP, source port, destination IP, destination port) and makes a forwarding decision, often via NAT or direct-server-return, packet by packet or per-connection. It has no idea if the payload is GET /api/users or a binary protocol. An L7 (application-layer) balancer is fundamentally a proxy: it terminates the client's TCP connection, buffers and parses the full HTTP request, and only then opens a brand-new, separate TCP connection to the chosen backend.

L4 versus L7 load balancerL4: forwards packets, one connection end to endclientL4 load balancerreads IP:port onlybackendTCPsame TCP flowL7: two separate connections, understands HTTPclientL7 load balancerends TLS, reads path / host / cookiesbackendTCP + TLSnew connection
An L4 balancer only looks at addresses and ports and passes the TCP flow through, so it's very cheap (microseconds per packet) but can't route by URL. An L7 balancer terminates the client's connection, parses the HTTP request, picks a backend by path, host or cookie, and opens its own connection to it.
L4L7
Show text version
L4 (transport pass-through)
Client --TCP--> [ L4 LB: reads IP:port only ] --TCP(same conn/NAT)--> Backend
                 no HTTP parsing, ~microseconds of work per packet

L7 (terminate + re-originate)
Client --TCP/TLS--> [ L7 LB: terminates, parses HTTP,
                        inspects path/host/headers/cookies,
                        picks backend, opens NEW connection ] --TCP/TLS--> Backend
                 two separate connections, CPU spent on parsing + often TLS

This is why L4 can push millions of packets/sec on modest hardware (it's doing header inspection, not parsing), while L7 costs meaningfully more CPU per request — you're paying for a full HTTP parse, potentially a TLS handshake, and a second connection setup to the backend.

Deep Dive: Load Balancing Algorithms

Consistent hashing ringB1B2B3B4B5B5 takes only this arcmod-N: hash(key) % 3 → % 4≈ 75% of keys move → cache stampedering: each key goes to the nextnode clockwise; adding B5 movesonly the keys in B5's arc
Keys and backends are hashed onto the same circle, and each key belongs to the next backend clockwise. Adding a backend only takes over the keys in its own slice of the ring; with plain mod-N hashing, changing N reshuffles almost everything.
new backend and the keys it takes
Show text version
Naive mod-N hashing when N changes (3 -> 4 backends):
  hash(key) % 3  vs  hash(key) % 4   -> ~75% of keys remap. Cache stampede.

Consistent hashing ring:
        B1
     /       \
   B4         B2
     \       /
        B3
  Adding B5 only steals a slice of ring space from its neighbor —
  most keys keep mapping to the same backend they always did.

Key Pointers

How It Works

  1. Client connection arrives at the load balancer's virtual IP.
  2. At L4, the balancer inspects only IP/port info and forwards packets to a backend chosen by its algorithm, without terminating the TCP session semantically.
  3. At L7, the balancer terminates the client's TCP/TLS connection and parses the HTTP request.
  4. L7 evaluates routing rules (host, path, headers) plus the load-balancing algorithm to pick a backend, then opens a separate connection to it.
  5. The response flows back through the balancer to the client.
  6. Health checks run continuously in the background — active checks poll a health endpoint on a timer, passive checks watch live request failures — pulling unhealthy backends out of rotation without operator intervention.

Try It Yourself

# Watch DNS/connection distribution across repeated requests to a
# load-balanced host — look for different response headers (e.g.
# X-Served-By, X-Backend-Id) that reveal which instance answered
for i in $(seq 1 10); do curl -s -D - -o /dev/null https://example.com/ | grep -i "x-served\|x-backend"; done

# Inspect L7 routing behavior directly with verbose curl — watch the
# TLS handshake and which Host/path headers you're actually sending
curl -v https://example.com/api/users

# Load-test to see the LB's algorithm reveal itself under concurrency;
# with least-connections, response times stay flatter under load than
# with naive round robin against heterogeneous backends
hey -z 30s -c 50 https://example.com/

# Simulate a backend dying and watch health checks pull it out of
# rotation — kill one instance in a local docker-compose LB setup
# and curl repeatedly; failed instance should stop receiving traffic
# within one health-check interval

Interview Q&A

Q: When would you choose L4 over L7, and vice versa?

A: L4 when you need raw throughput and the traffic isn't HTTP, or when you explicitly don't want to pay parsing/TLS-termination cost at the balancer — databases, gRPC over raw TCP at extreme scale, gaming/UDP traffic. L7 when routing decisions depend on the request itself: path-based service routing, canary releases by header, cookie-based session affinity, or when you want the balancer to also handle TLS termination and compression centrally instead of duplicating that in every backend.

Q: What happens when one backend server dies?

A: Health checks detect it and the load balancer stops routing new traffic to it. Active health checks poll a dedicated endpoint (like /healthz) on an interval and mark the backend unhealthy after N consecutive failures; passive checks infer unhealthiness from live traffic — a spike in connection resets or 5xx responses from that backend. Once marked unhealthy, the LB removes it from the rotation set until it starts passing checks again.

Q: Follow-up — what if the health check itself is misleading, e.g. the process is alive but the database connection pool behind it is exhausted?

A: This is why a good health check endpoint does more than return 200 for "process is up" — it should verify the backend can actually serve traffic (e.g. check DB connectivity, dependency reachability) without being so heavy that the check itself becomes a load problem. Teams often implement a "readiness" check (can this instance serve traffic right now) separate from a "liveness" check (is the process alive at all), which is exactly the Kubernetes readiness/liveness probe split.

Q: Follow-up — if health checks are slow to detect failure, what's still happening to users during that gap?

A: Requests routed to the dying backend during the detection window time out or error at the client, which is why timeouts on the caller side and retries with backoff (to a different, healthy backend, ideally) matter as a second line of defense — the LB's health check is not instantaneous, so the client-side resilience layer has to absorb the gap.

Q: Why is consistent hashing preferred over hash(key) % N for cache routing?

A: Because with plain modulo hashing, adding or removing even one backend changes the result of the modulo for the vast majority of keys, causing a mass cache miss/remap event ("cache stampede") right when the system is already in a state of change (scaling event). Consistent hashing on a ring only remaps the fraction of keys adjacent to the added/removed node, keeping cache hit rates stable during scaling.

System Design Tie-in

This shows up immediately in almost any "design a scalable web service" prompt: once you draw more than one backend instance, the interviewer expects you to explicitly place a load balancer in front of them and justify L4 vs L7. For an API gateway design, you'd reach for L7 to route /users, /orders, /payments to different microservices from one entry point. For a design involving sharded caches or consistent-hash-partitioned data stores (a distributed cache, a chat system's connection routing, a sharded database proxy), you'd specifically call out consistent hashing to justify why adding a node doesn't invalidate the whole cache. And whenever you're asked "how do you detect and handle an unhealthy instance," the health-check mechanism described here is the expected answer.

Trade-offs

L4 is faster and cheaper per connection but has zero application awareness — it can't route by URL path or do cookie-based session affinity at the HTTP layer. L7 is flexible and content-aware but costs more CPU per request and adds a hop of processing latency, plus doubles the number of TCP (and possibly TLS) connections in play. Real infrastructure often layers both: a cloud-provider L4 balancer absorbs raw connection volume and DDoS traffic at the edge, with an L7 proxy (Envoy, Nginx) behind it doing smart, content-aware routing to services. Consistent hashing trades a small amount of routing simplicity for much better stability under cluster resizing — worth it any time cache affinity or shard ownership matters, overkill for simple stateless round-robin traffic.