6.4 Session Performance: Caches, Tickets, and Stapling
A full TLS 1.3 handshake is one round trip; TLS 1.2 is two. Multiply by every connection, and crypto becomes your latency line item — unless the parties remember each other.
Intermediate
What you will learn
What a TLS session costs, and what resumption actually skips.
Session IDs +
ssl_session_cache— server-side memory, shared across workers.Session tickets — client-side state, and the forward-secrecy debate.
OCSP stapling — closing the revocation latency gap at the edge.
Sizing, verification, and the numbers you should expect.
1. The cost of a handshake
The two round trips (TLS 1.2) or one (TLS 1.3) each cost a network RTT before the first HTTP byte. On a 40 ms edge-to-client path, TLS 1.2 adds ~80 ms of pure setup to every fresh connection — plus asymmetric crypto (ECDHE) that's thousands of times costlier than the symmetric bulk work that follows. At scale, two levers dominate everything else:
Reuse connections (HTTP/2's multiplexing; keep-alives — Module 9 makes this systematic), and
resume sessions when connections must be rebuilt.
Resumption skips the expensive part. The client proves it was here before (session ID or ticket), the server confirms, both sides derive the same keys from the remembered secret — no fresh certificate verification, no full key exchange. TLS 1.2 resumption: one RTT. TLS 1.3: PSK modes, including 0-RTT where the first flight
carries application data (with replay caveats —
ssl_early_data is the opt-in).
2. Session IDs and the shared cache
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;The classic mechanism: the server keeps session state in memory keyed by a session ID; the client presents the ID; the server looks it up. The NGINX directives:
shared:SSL:10m— a shared-memory zone (Lesson 1.4's blackboard!) holding ~40 000 sessions, shared across all workers — any worker can resume any client's session. The alternatives (off,builtinper-worker) are mostly historical;sharedis the production answer. One zone per unique cache name: multiple TLS servers can share one (shared:SSL:10meverywhere) or isolate (shared:per-site:10m).ssl_session_timeout 10m— how long entries live (default 5m). Longer = more resumption, more stale-session risk; the classic trade-off lands at 10–60 minutes for most sites.
Resumption, proven mechanically — session saved to a file, then re-presented:
$ echo | openssl s_client -connect 127.0.0.1:8643 -servername app.example.test -tls1_2 \
-sess_out /tmp/sess.pem >/dev/null
$ echo | openssl s_client -connect 127.0.0.1:8643 -servername app.example.test -tls1_2 \
-sess_in /tmp/sess.pem 2>/dev/null | grep -E "Reused|New"
Reused, TLSv1.2, Cipher is ECDHE-RSA-AES128-GCM-SHA256
Reused — the second handshake cost one round trip and no ECDHE. And the access log agrees: the
$ssl_session_reused column from Lesson 6.1 flips from
. to
r for resumed connections — your resumption
rate is a log query, not a mystery.
3. Session tickets — and the great trade-off
ssl_session_tickets on; # our settingTickets invert the storage: the server encrypts its session state with a ticket key and hands the blob to the client; the client returns it verbatim; the server decrypts and resumes. No server-side memory at all — horizontal scale for free (any node with the key can resume any ticket).
The forward-secrecy debate, stated honestly: if a ticket key leaks, every session it protected becomes decryptable after the fact — weakening the ECDHE property you chose ciphers for in Lesson 6.2. Modern practice threads the needle:
rotate ticket keys frequently (
ssl_session_ticket_keyfiles, rotated by cron/secret manager, shared across nodes);or disable tickets on the highest-security endpoints (
ssl_session_tickets off— accepting cache-only resumption).
TLS 1.3's PSK-resumption inherits the same reasoning — resumption is only as forward-secret as the lifetime of the secret it's built on. Pick your posture consciously; the example ships
on with a comment because that's the mainstream default, and the lesson exists so you know what you're choosing.
4. OCSP stapling — the revocation answer
The certificate question clients can't stop asking: "has the CA revoked this cert?" The default (OCSP) makes each client's first handshake wait on a third-party CA responder — latency and privacy leakage (the CA sees who's visiting you). Stapling flips it:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate certs/chain.pem; # intermediates for verification
resolver 127.0.0.1 valid=300s; # needed to reach the OCSP URLNGINX fetches the OCSP response itself (periodically, off the request path), verifies it, and staples it into its own handshakes — clients get revocation status from you, instantly, privately. The browser sees one fewer round trip on first visit; the CA sees the curator, not your users.
This lesson documents but doesn't enable it (the example CA has no OCSP responder — enabling it would be cargo cult with warning noise), which is itself the lesson:
stapling is only as good as the responder behind it. In production with public CAs — though Let's Encrypt retired its OCSP responder in 2025 and serves CRLs now; check your CA's posture — it's two lines and a
resolver, and your verify ritual gains
openssl s_client -status → "OCSP Response Status: successful".
5. Sizing and the numbers to expect
Knob | Sane start | Meaning |
|---|---|---|
| 10m ≈ 40k sessions | size ≈ peak concurrent-TLS-clients × safety |
| 10m | resumption window |
tickets | on (rotated keys) / off (max FS) | storage vs scale trade |
stapling | on + verify | revocation latency + privacy |
What good looks like in the field: for returning-client populations, resumption rates above 50 % (watch
$ssl_session_reused) and handshake times (your APM's
ssl phase) well under one RTT. When they're not: check that your pool shares cache/ticket keys (a load-balanced fleet where each node tickets with different keys resumes
nothing), and that nothing between you and the client strips session state (some middleboxes do — measure, don't assume).
Try it yourself
The §2 proof — save and resume a session; check
Reused, then find the matchingreused=rlines in the access log by making the same journey with curl (curl -vtwice on one command line reuses the connection — that's connection reuse; the s_client dance is session reuse — know the difference!).Cache isolation experiment. Give the mTLS server its own cache name (
shared:MTLS:5m), reload, resume across servers — sessions don't cross cache names. Now point both atshared:SSL:10mand watch them share.Timeout economics. Set
ssl_session_timeout 30s, resume after 10 s (works), after 60 s (full handshake) — the window, measured.(If you have a public-cert server anywhere) run the stapling ritual:
openssl s_client -connect … -statusand read the OCSP response block.
Q&A
Q1. What exactly does resumption skip, and what does it never skip?
Answer
Skips: certificate chain verification and the fresh (ECDHE) key exchange — the expensive asymmetric work. Never skips: the symmetric record protection, certificate presence in the handshake, and (in 1.3's 0-RTT aside) replay-risk analysis. The connection is still encrypted from byte one.
Q2. Why does
ssl_session_cache shared: beat per-worker caching on a real fleet?
Answer
Clients don't stick to workers — without a shared zone, a resumed session lands on a worker that doesn't know it and falls back to a full handshake. The shared zone is the cross-worker blackboard resumption needs.
Q3. The forward-secrecy argument against long-lived ticket keys, in two sentences.
Answer
Tickets encrypt session state under a server-held key; if that key is later compromised, all recorded ticket-protected traffic becomes decryptable — voiding ECDHE's "past traffic stays safe" guarantee. Short rotation windows bound the damage;
off removes it at the cost of server-side-only resumption.
Q4. Your fleet behind an LB shows ~0 % resumption despite correct config on every node. First suspect?
Answer
Fleet key/cache inconsistency: ticket keys or cache names differ per node (or per deploy), so the node a client returns to doesn't recognize the session. Verify shared ticket keys and one cache name across the pool — the "narrowest node" law.
Takeaways
Handshakes cost RTTs + asymmetric crypto; resumption trades memory(cache) or client-held blobs (tickets) for one-RTT setups.
ssl_session_cache shared:across workers (and nodes!) is the default answer;$ssl_session_reusedmakes your resumption rate a measurable metric.Tickets scale statelessly but inherit their forward secrecy from ticket-key hygiene — rotate or refuse, consciously.
OCSP stapling moves revocation checks off your users' critical path and out of their privacy — two lines plus a resolver, when a real responder exists.
Fleet consistency is the silent killer: resumption is only as wide as the narrowest node sharing its secrets.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.