Skip to main content

TLS Termination & Connection Management

Level 4: Level 4: Scaling Compute & Traffichard30 mintlsconnection-management

Terminate TLS at the edge and re-encrypt with mTLS inside, pool and keep-alive connections, and fix H2/gRPC/WebSocket pinning with client-side or per-stream balancing.

Where do you decrypt, and how many connections do you hold?

Two questions decide the health of a system's front door under real load: where do you decrypt, and how many TCP connections does your backend actually have to hold open? Getting either wrong shows up as CPU burn, port exhaustion, or a load balancer that silently stops balancing.

Where you terminate TLS

The TLS handshake is expensive, and it happens per new connection. Edge/LB termination decrypts at the load balancer or edge PoP, so the backend fleet never pays the handshake cost and certificates are managed in one place. The gap: traffic from the LB to the backend is now plaintext internally. End-to-end TLS (or re-encryption at the LB) keeps it encrypted all the way, and mTLS additionally has both sides present certificates so services mutually authenticate: the zero-trust default inside a service mesh (Istio/Linkerd issue and rotate certs via sidecars). A common production shape: TLS terminated at the edge for the public handshake, then re-encrypted with mTLS for the internal hop, giving both cheap edge offload and a zero-trust interior.

Check yourself

Match each statement to the TLS setup it describes.

Backends never pay handshake CPU, but the internal hop is plaintext
Every hop is encrypted, but services do not prove their identity to each other
Both sides present certificates: the zero-trust default inside a mesh
Sidecars issue and rotate its certificates automatically

SNI and certificates at scale. One IP/LB often fronts many hostnames. SNI (the client sends the target hostname in the ClientHello) lets the LB pick the right certificate per connection. Certificate rotation must be automated (ACME/Let's Encrypt, an internal CA); manual cert management does not survive thousands of short-lived certs.

Connection management: where the sharp edges live

Every new backend connection means a handshake and consumes an ephemeral port. A proxy opening a fresh connection per request will exhaust its ~64K ephemeral ports and burn CPU on handshakes. The fixes are keep-alive (reuse a connection for many requests) and connection pooling (a warm pool per backend, multiplexing requests over it), making backend connection counts a function of concurrency and pool size, not raw request rate.

Check yourself
A gRPC fleet behind an L7 load balancer scales from 5 pods to 15 during a spike. Existing clients each hold one long-lived HTTP/2 connection. Where do their RPCs land?

The trap: multiplexed connections pin to one backend

HTTP/2 and gRPC multiplex many streams over one long-lived connection. A layer-7 load balancer balances at connection establishment. But a gRPC client opens one connection and keeps it, sending thousands of RPCs as streams over that single connection, all pinned to whatever backend the LB picked at connect time. Add ten new backend pods and existing clients keep hammering the old ones; the new pods sit idle.

gRPC client --- one long-lived H2 connection ---> backend A  (all streams pinned)
new backends B,C come up  ->  get zero traffic until clients reconnect

The fixes: client-side load balancing (the client spreads RPCs itself, via gRPC's round_robin resolver or an xDS/mesh control plane), balance at L7 per-request with a proxy that understands H2 streams (Envoy balances individual streams, not just connections), or periodically cycle connections (max-connection-age) so clients re-resolve and rebalance. WebSockets have the same pinning problem, so plan for connection draining and rebalancing on scale events.

C10k / C10M. Holding 100K-plus concurrent connections needs event-driven proxies (epoll/kqueue, nginx/Envoy) rather than thread-per-connection, plus OS tuning (file-descriptor limits, ephemeral port range, TCP buffers, SO_REUSEPORT).

Interview nuance: if you say "terminate gRPC at our L7 load balancer" without mentioning stream pinning, expect "then why did your new pods get no traffic after a scale-up?" Naming client-side LB or Envoy per-request balancing is what proves you have run this in production.

Recap: terminate TLS at the edge and re-encrypt with mTLS for a zero-trust interior, pool and keep-alive connections to avoid handshake cost and port exhaustion, and remember that long-lived multiplexed H2/gRPC/WebSocket connections pin to one backend so you need client-side or per-request balancing to actually spread load.

Check yourself
You designed the front door: TLS terminates at the edge, mTLS inside, keep-alive pools to the backends. After a scale-up the new pods stay cold while the old pods run hot. Which choice is biting you?

Apply

Your turn

The task this lesson builds to.

Decide where to terminate TLS for an API platform and how to keep backend connection counts sane at 100k concurrent clients.

Think about

  1. What is the tradeoff of edge TLS termination vs end-to-end/mTLS?
  2. Why do long-lived multiplexed gRPC/WebSocket connections defeat L7 balancing?
  3. How do pooling and keep-alive avoid port exhaustion?

Practice

Make it stick

A second problem on the same idea, so it survives past today.

Design connection and TLS management for a real-time trading or chat platform holding 5 million concurrent WebSocket connections across a fleet, where backends scale up and down through the day and a dropped connection is a user-visible event. Lead with how you spread and rebalance those long-lived connections.

Think about

  1. Why does L7 rebalancing not help an already-open WebSocket?
  2. How do new nodes ever acquire connections after a scale-up?
  3. What makes a scale-down drain invisible instead of dropping 300K users at once?