Part 7 — Layer 5: Session
Job
Establish, maintain, synchronise, resume, and end a dialogue between two applications: something that lasts longer than a single packet or single request.
The honest truth
TCP/IP has no dedicated session layer protocol. Session responsibilities are spread across TLS, HTTP, and your application. That's exactly why session problems are so often nobody's problem until they become an incident.
In our story
Five different "sessions" are active at once when you click Place order:
| "Session" | Lifetime | What establishes it | What it buys you |
|---|---|---|---|
| TCP connection | Seconds to minutes | 3-way handshake | Reliable byte stream (strictly L4, but the browser treats it as a reusable session) |
| TLS session | Hours (via tickets) | TLS handshake; resumption with session tickets / PSK | Skip the expensive handshake work next time |
| HTTP/2 connection + streams | Minutes | ALPN negotiation (h2), then SETTINGS frames |
Many concurrent requests (streams) multiplexed over one TCP+TLS connection |
| Browser connection pool | Until idle timeout | Browser policy | Your click reuses the connection opened when the cart page loaded, so no new TCP or TLS handshake is needed |
| Application login session | Days | Login → Set-Cookie: session=… or a JWT |
The server knows who you are on every stateless HTTP request |
So in the common case, your click skips the TCP and TLS handshakes entirely:
the browser already has a warm HTTP/2 connection to shop.example.com from
loading the checkout page. The POST becomes a new stream (stream ID 15, say)
on that existing connection.
Two more session-level behaviours happen on the server side:
- The ALB keeps its own pool of keep-alive connections to the pods, which is a separate session layer behind the proxy.
- If the ALB has sticky sessions enabled, a cookie (
AWSALB=…) pins you to one pod.
How it works
1. Session resumption (TLS). After a full handshake the server gives the client an encrypted session ticket. On reconnecting, the client presents it and both sides derive keys without repeating the certificate exchange. TLS 1.3 even allows 0-RTT data, where the request goes in the very first flight, but 0-RTT data can be replayed by an attacker, so it's only safe for idempotent requests (GET, never POST /orders).
2. Multiplexing (HTTP/2). One connection carries many independent streams, each with its own ID and flow control. That removes HTTP/1.1's "one request at a time per connection" limit. The catch is TCP-level head-of-line blocking: one lost TCP packet stalls all streams until it's retransmitted. HTTP/3 (QUIC over UDP) fixes this by doing loss recovery per stream.
3. Dialogue control and checkpointing. These are classic OSI session functions. Modern equivalents:
- Resumable uploads (tus protocol, S3 multipart): restart from the last checkpoint after a disconnect.
- WebSocket / gRPC streams: long-lived, full-duplex dialogues with their own keepalive pings.
- Database sessions: Postgres connection state (transactions,
SETvariables, prepared statements) is session state. That's why transaction-mode poolers like PgBouncer breakSETand session-level prepared statements.
4. Application sessions: stateful vs stateless.
| Approach | Where state lives | Trade-off |
|---|---|---|
| Server-side session (cookie holds an ID → Redis) | Shared store | Easy revocation; extra lookup per request; the store is a dependency |
| Sticky sessions (cookie → specific instance) | Instance memory | Simple; breaks on deploys/scale-in, uneven load |
| Token (JWT holds the claims) | The client | No lookup; hard to revoke; token size added to every request |
Try it yourself
# See ALPN (h2 vs http/1.1), TLS version, and whether the TLS session was reused
curl -sv -o /dev/null https://shop.example.com/ 2>&1 | grep -E 'ALPN|SSL connection|HTTP/'
# Two requests in one curl invocation: watch the connection get reused
curl -sv -o /dev/null -o /dev/null https://shop.example.com/ https://shop.example.com/health 2>&1 \
| grep -E 'Connected to|Re-using|left intact'
# TLS session resumption test (look for "Reused" on later connections)
openssl s_client -connect shop.example.com:443 -servername shop.example.com -reconnect </dev/null 2>/dev/null \
| grep -E '^(New|Reused)'
# Browser: chrome://net-internals/#sockets and DevTools → Network → "Connection ID" column
What breaks
| Symptom | Session-level cause |
|---|---|
| Sporadic 502s from the load balancer, especially after idle periods | Keep-alive timeout mismatch: the backend closes an idle connection the LB still thinks is open (see war story) |
| Users logged out during a deploy | Sessions stored in instance memory; sticky sessions pointed at terminated instances |
| One pod at 90% CPU, others idle | Sticky sessions or long-lived HTTP/2 / gRPC connections pinning traffic |
| WebSocket drops every ~60 s | An idle timeout somewhere on the path (LB, NAT, corporate proxy) is shorter than your ping interval |
| "Prepared statement does not exist" errors | Transaction-mode connection pooler + session-level DB features |
War story: "The 502s nobody could reproduce"
A Node.js service behind an ALB returned about 0.05% 502s with no errors in its
own logs. Cause: the ALB's idle timeout was 60 s, but Node's
server.keepAliveTimeout defaulted to 5 s. After 5 s of idleness, Node
closed the backend connection. Sometimes the ALB had just sent a new request
on that same connection at that exact moment. The request hit a closing socket,
and the ALB returned 502. Fix: make the server's keep-alive timeout
longer than the load balancer's idle timeout (for example 65 s vs 60 s), so
the LB always closes first. Lesson (and a good interview question): in any
proxy chain, the side that sends requests on a reused connection must be the
one that closes idle connections first, so every hop's idle timeout must be
shorter than the timeout of the hop behind it.
Why it matters to you
- Connection reuse is the biggest session-layer performance lever. A warm connection avoids 2 RTTs (TCP + TLS) and TCP slow start.
- Timeouts along a proxy chain are a design artefact. Document them per hop (client → CDN → LB → sidecar → app → DB).
- Prefer stateless app servers with an external session store or tokens. Sticky sessions are technical debt that shows up at deploy time.
- Long-lived connections defeat load balancing. gRPC/HTTP/2 clients on an L4 balancer send everything to one backend. See Part 15.
Go deeper in the series
- The keep-alive 502 race and timeout nesting: TCP/IP guide, Chapter 57.
- TLS session resumption, tested from Go: TCP/IP Chapter 29 (it resumes only after the client has read the session ticket).
- Long-lived sessions (SSE, WebSockets) and the timeouts that cut them: HTTPS guide, Chapter 24.