Skip to main content
fast by design

Layer 4 vs Layer 7: The Load Balancing Trade-off

8 min read Chapter 74 of 90

Layer 4 vs Layer 7: The Load Balancing Trade-off

The main chapter showed Nginx adding 0.12ms at P50 and 0.45ms at P99 operating at Layer 7. Layer 4 load balancing (TCP pass-through) can reduce this to near zero, but gives up HTTP-aware routing, header inspection, and response caching. This section quantifies the exact cost of each Layer 7 feature and determines which features justify their latency cost for the content platform.

How Layer 4 and Layer 7 Differ

Layer 4 operates on TCP segments. It sees source/destination IP, source/destination port, and SYN/FIN flags. It cannot inspect HTTP headers, URL paths, or request bodies. It forwards raw bytes without interpretation:

Layer 4 (TCP pass-through):
  Client ←──TCP──→ Load Balancer ←──TCP──→ Backend
                   (forwards bytes)

  What it CAN do:
    - Route by destination port
    - Route by source IP (geo-based)
    - Distribute connections round-robin or least-connections
    - Health check via TCP connect (port open = healthy)

  What it CANNOT do:
    - Route by URL path (/api/* vs /static/*)
    - Route by HTTP header (Authorization, Content-Type)
    - Terminate TLS (client TLS goes to backend directly)
    - Inspect or modify HTTP responses
    - Cache responses
    - Rate limit by URL or user

Layer 7 (HTTP routing):
  Client ←──TLS──→ Load Balancer ←──HTTP──→ Backend
                   (terminates TLS,
                    parses HTTP,
                    makes routing decisions)

  Additional capabilities:
    - Path-based routing
    - Header-based routing (canary deployments, A/B testing)
    - Request/response modification
    - Response caching
    - Rate limiting by URL/user
    - WebSocket upgrade handling
    - HTTP/2 to HTTP/1.1 translation

Benchmark: L4 vs L7 Latency

# Test setup:
# - HAProxy in TCP mode (L4) vs Nginx in HTTP mode (L7)
# - Backend: content platform article API (Java, returning 5KB JSON)
# - Load: wrk2 at constant 10,000 RPS, 4 threads, 200 connections
# - Duration: 120 seconds per test
# - Hardware: 4-core VM for proxy, 8-core VM for backend

# L4 (HAProxy TCP mode):
wrk2 -t4 -c200 -d120s -R10000 --latency http://haproxy-l4:80/api/articles/12345

# L7 (Nginx HTTP mode, minimal config):
wrk2 -t4 -c200 -d120s -R10000 --latency http://nginx-l7:80/api/articles/12345

# L7 (Nginx HTTP mode, full production config):
wrk2 -t4 -c200 -d120s -R10000 --latency http://nginx-l7-full:80/api/articles/12345
Results:
┌────────────────────────────────┬─────────┬─────────┬──────────┬──────────┐
│ Configuration                  │ P50     │ P95     │ P99      │ P99.9    │
├────────────────────────────────┼─────────┼─────────┼──────────┼──────────┤
│ Direct (no proxy)              │ 11.2ms  │ 18.1ms  │ 28.4ms   │ 42.1ms   │
│ L4 TCP pass-through (HAProxy) │ 11.3ms  │ 18.4ms  │ 28.9ms   │ 43.2ms   │
│ L7 minimal (Nginx)            │ 11.4ms  │ 18.8ms  │ 29.6ms   │ 46.8ms   │
│ L7 full config (Nginx)        │ 11.5ms  │ 19.2ms  │ 30.2ms   │ 51.3ms   │
│ L7 with regex routing         │ 11.6ms  │ 19.5ms  │ 31.8ms   │ 55.4ms   │
│ L7 with Lua scripting         │ 12.1ms  │ 21.3ms  │ 38.7ms   │ 78.2ms   │
└────────────────────────────────┴─────────┴─────────┴──────────┴──────────┘

Added latency over direct connection:
  L4: +0.1ms P50, +0.5ms P99      (TCP forwarding overhead only)
  L7 minimal: +0.2ms P50, +1.2ms P99   (HTTP parse + route)
  L7 full: +0.3ms P50, +1.8ms P99      (parse + route + headers + buffer + log)
  L7 regex: +0.4ms P50, +3.4ms P99     (regex matching adds tail latency)
  L7 Lua: +0.9ms P50, +10.3ms P99      (Lua VM adds significant tail latency)

The key insight: L7 adds latency proportional to the complexity of HTTP processing. Each feature has a measurable cost.

Feature-by-Feature Cost Breakdown

# Isolating the cost of each L7 feature:
# Baseline: minimal proxy_pass (parse HTTP, forward, return)

# Feature 1: Path-based routing with exact match
# Cost: +0.01ms P50, +0.02ms P99
location = /api/articles {
    proxy_pass http://article_backend;
}

# Feature 2: Path-based routing with prefix match
# Cost: +0.01ms P50, +0.03ms P99
location /api/ {
    proxy_pass http://article_backend;
}

# Feature 3: Path-based routing with regex
# Cost: +0.05ms P50, +0.5ms P99 (regex compilation is cached, but matching varies)
location ~ ^/api/articles/([0-9]+)$ {
    proxy_pass http://article_backend;
}

# Feature 4: Header inspection (single header check)
# Cost: +0.02ms P50, +0.05ms P99
map $http_x_canary $backend {
    "true"   canary_backend;
    default  article_backend;
}

# Feature 5: Request body inspection (e.g., for routing based on JSON field)
# Cost: +0.5-5ms P50 (must buffer and parse body)
# AVOID: This is extremely expensive and defeats streaming

# Feature 6: Access logging (buffered)
# Cost: +0.01ms P50, +0.05ms P99 (async write to buffer)
access_log /var/log/nginx/access.log combined buffer=64k;

# Feature 7: Access logging (unbuffered/sync)
# Cost: +0.1ms P50, +2ms P99 (blocks on disk write)
access_log /var/log/nginx/access.log combined;  # No buffer = sync

# Feature 8: Response modification (add headers)
# Cost: +0.005ms P50, +0.01ms P99
add_header X-Request-ID $request_id;
add_header X-Backend-Server $upstream_addr;

# Feature 9: Gzip compression (already covered in CH23, but for completeness)
# Cost: +0.1-2ms depending on response size and level
gzip on;
gzip_comp_level 4;

When to Use Layer 4

Layer 4 is optimal when you need minimum latency and do not require HTTP-aware features:

L4 optimal scenarios for the content platform:
  1. Service-to-service communication within the datacenter
     - Services already handle their own TLS
     - Routing is simple (each service has one endpoint)
     - 0.1ms savings per hop * 4 hops = 0.4ms saved per article render

  2. Database connection proxying
     - TCP pass-through to PostgreSQL/Redis
     - No HTTP headers to inspect
     - Connection pooling handled by application (PgBouncer, etc.)

  3. gRPC services
     - HTTP/2 multiplexing requires end-to-end HTTP/2
     - L4 preserves client HTTP/2 connection to backend
     - L7 would terminate and re-establish HTTP/2 (double overhead)

  4. WebSocket connections
     - After upgrade, WebSocket is raw TCP frames
     - L7 proxy must understand WebSocket framing (overhead)
     - L4 just passes bytes (zero overhead after connection)
# HAProxy L4 configuration for inter-service communication:
frontend service_mesh
    mode tcp
    bind *:9000
    default_backend article_services

backend article_services
    mode tcp
    balance leastconn
    server article-1 10.0.2.10:8080 check inter 5s fall 3 rise 2
    server article-2 10.0.2.11:8080 check inter 5s fall 3 rise 2
    server article-3 10.0.2.12:8080 check inter 5s fall 3 rise 2

When to Use Layer 7

Layer 7 is required when routing decisions depend on HTTP content:

L7 required scenarios for the content platform:
  1. Edge proxy (internet-facing)
     - TLS termination (certificates managed centrally)
     - Path routing: /api/* → backend, /assets/* → CDN origin
     - Security headers injection (CSP, HSTS)
     - Rate limiting by client/API key
     - Bot detection and blocking

  2. Canary deployments
     - Route 5% of traffic to new version based on header/cookie
     - Requires HTTP header inspection
     - L4 cannot selectively route by HTTP content

  3. A/B testing
     - Route users to different backends based on cookie value
     - Requires cookie parsing (HTTP-level operation)

  4. API versioning
     - /v1/articles → legacy backend
     - /v2/articles → new backend
     - Path-based routing requires HTTP parsing

Optimizing Layer 7 for Minimum Overhead

When L7 is required, minimize its cost:

# FAST: Optimized L7 configuration for minimum added latency

# 1. Use exact matches before prefix matches before regex
location = /health { return 200 "ok"; }    # 0.005ms (fastest)
location /api/ { proxy_pass ...; }          # 0.01ms (prefix tree lookup)
location ~ regex { proxy_pass ...; }        # 0.05ms+ (avoid if possible)

# 2. Limit header processing
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Do NOT add unnecessary headers (each add_header costs ~5us)

# 3. Use buffered logging (never sync)
access_log /var/log/nginx/access.log combined buffer=64k flush=5s;

# 4. Disable features you do not use
proxy_redirect off;            # Skip response header rewriting
proxy_intercept_errors off;    # Skip error page processing
proxy_ignore_client_abort on;  # Do not abort backend on client disconnect

# 5. Tune worker processes and connections
worker_processes auto;                # One per CPU core
worker_rlimit_nofile 65535;
events {
    worker_connections 16384;         # Max connections per worker
    multi_accept on;                  # Accept all pending connections at once
    use epoll;                        # Linux epoll (O(1) event notification)
}

Connection Affinity and Session Persistence

Some requests must go to the same backend (stateful sessions, local caches). L7 can implement this via cookies:

# Session persistence via cookie (L7 only):
upstream article_backend {
    server 10.0.2.10:8080;
    server 10.0.2.11:8080;
    server 10.0.2.12:8080;

    # Nginx Plus: sticky cookie
    # sticky cookie srv_id expires=1h path=/;
}

# For Nginx OSS: use ip_hash (less flexible)
upstream article_backend_sticky {
    ip_hash;
    server 10.0.2.10:8080;
    server 10.0.2.11:8080;
    server 10.0.2.12:8080;
}
Affinity cost:
  Round-robin (no affinity):     even distribution, 0ms overhead
  IP hash:                       uneven if NAT concentrates IPs, 0.001ms overhead
  Cookie-based (Nginx Plus):     even distribution, 0.02ms overhead (cookie parse)

Content platform: no affinity needed
  - Application is stateless (session in Redis)
  - Local caches are warmed per-pod (cache miss on first request acceptable)
  - Round-robin provides best latency distribution

Hybrid Architecture: L4 External + L7 Internal

The content platform uses a hybrid approach that minimizes latency while preserving L7 features where needed:

Internet → L4 Load Balancer (TCP pass-through to Nginx)
             → Nginx L7 (TLS termination, routing, security)
                → Backend services (application logic)

Why not L7 at the edge?
  - Cloud L4 LB (AWS NLB, GCP TCP LB) adds 0.05ms
  - Cloud L7 LB (AWS ALB, GCP HTTP LB) adds 0.5-2ms
  - Running our own Nginx gives us control over configuration
  - L4 edge distributes across multiple Nginx instances (HA)

Latency path:
  Client → NLB (L4): +0.05ms
  NLB → Nginx (L7):  +0.3ms (full config)
  Nginx → Backend:   +0.02ms (keepalive)
  Total proxy overhead: 0.37ms

Compared to cloud ALB alone: ~1.5ms
Savings: 1.13ms per request (at 50,000 req/min = 940 CPU-seconds saved/min)

The L4 vs L7 choice is not binary. Use L4 where you can (inter-service, databases, gRPC), L7 where you must (edge routing, security, canary), and measure the cost of every L7 feature you enable. For the content platform, the hybrid approach delivers sub-millisecond proxy overhead while maintaining full routing flexibility at the edge.