skip to content

An nginx TLS terminator is burning CPU on full handshakes for repeat visitors. What do `ssl_session_cache`, `ssl_session_timeout` and `ssl_session_tickets` do, and which nginx default most often explains it?

level: middleimportance: should knowfreq 45%

answer

  1. the default is none, not a cache
  2. builtin lives inside one worker
  3. a shared zone spans workers, not hosts
  4. tickets are stateless, keys are per-node
  5. log $ssl_session_reused and measure

basics

~10 s

They control TLS session resumption. nginx defaults ssl_session_cache to none, so no server-side sessions are stored; set shared:SSL:10m so all workers share one cache, raise ssl_session_timeout above its 5m default, and keep tickets on.

solid answer

~50 s

Resumption lets a returning client skip the expensive part of the handshake — the asymmetric key exchange and signature — by reusing keying material from an earlier connection. nginx offers two mechanisms. `ssl_session_cache` is server-side storage, and its default is `none`, meaning nginx tells clients sessions may be reused but stores nothing, so every connection is a full handshake. `shared:SSL:10m` creates a named zone shared by all worker processes — roughly 4,000 sessions per megabyte — which matters because `builtin` is OpenSSL's per-worker cache and a client landing on another worker misses it. `ssl_session_timeout` bounds how long a session may be resumed and defaults to just 5 minutes. `ssl_session_tickets` is the stateless mechanism, on by default: nginx encrypts the session state and hands it to the client. Across multiple nginx nodes, tickets only work if the nodes share `ssl_session_ticket_key` material.

code

nginx · 9 lines
nginx
http {
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets on;
    ssl_session_ticket_key /etc/nginx/tickets/current.key;
    ssl_session_ticket_key /etc/nginx/tickets/previous.key;

    log_format tls '$remote_addr $ssl_protocol reused=$ssl_session_reused';
}

go deeper

for a junior

Know that TLS resumption lets a returning client skip the expensive part of the handshake, and that ssl_session_cache shared:SSL:10m; is the line that enables server-side caching because the default stores nothing.

for a middle

Explain the two mechanisms side by side — shared-memory cache versus stateless tickets — and why builtin misses when a client lands on another worker while a named shared zone does not.

for a senior

Diagnose with evidence: log $ssl_session_reused, work out whether misses come from the default cache setting or from per-node ticket keys, and weigh the forward-secrecy cost of long timeouts and stale keys.

for a principal

Own ticket-key distribution and rotation as a secrets problem across the whole edge fleet, and decide how much handshake CPU you are willing to trade for shorter secret lifetimes.

## What resumption saves A full TLS handshake costs a round trip or two plus asymmetric cryptography — a signature with the server's private key and an ephemeral key exchange. On a busy terminator that private-key operation is the dominant CPU cost of TLS. Resumption skips it: the client presents evidence of an earlier session, both sides derive fresh keys from the retained secret, and the connection is up in one round trip (or zero, with TLS 1.3 early data). For a site with returning visitors, the difference between resuming and not is routinely a several-fold difference in handshake CPU. ## The stateful mechanism: ssl_session_cache ```nginx ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ``` The parameter forms matter: - **`none`** — the default. nginx tells clients that sessions may be reused, but stores nothing, so every attempt to resume falls back to a full handshake. It exists so that clients which dislike an outright refusal are not upset. This default is the usual answer to "why are we doing full handshakes?". - **`off`** — sessions strictly may not be reused, and nginx says so. - **`builtin:1000`** — OpenSSL's own cache, which lives **inside a worker process**. With several workers, a returning client handled by a different worker finds nothing. It also fragments memory. - **`shared:NAME:SIZE`** — a shared-memory zone visible to every worker. The nginx documentation puts capacity at roughly 4,000 sessions per megabyte, so `10m` holds on the order of 40,000. Use one name across all servers that should share the cache; `shared` and `builtin` may be listed together, but the shared zone is what does the work. `ssl_session_timeout` defaults to 5 minutes, which is short for a site people browse for half an hour. Raising it to a few hours or a day increases the hit rate; the trade is that a session secret usable for longer widens the window in which its compromise matters, which is the argument against setting it to a week. ## The stateless mechanism: session tickets With `ssl_session_tickets on;` (the default since nginx 1.5.9), nginx encrypts the session state under a ticket key and gives the ciphertext to the client, keeping no per-session server state. The client presents the ticket to resume. This scales beautifully — the memory cost is zero — and it is how most resumption happens in practice, including under TLS 1.3, where resumption is expressed as a pre-shared key delivered in a ticket. The operational catch appears the moment you have more than one nginx: each instance generates a random ticket key at startup, so a client that lands on node B cannot use a ticket issued by node A, and you silently perform full handshakes for a large share of traffic behind a round-robin load balancer. `ssl_session_ticket_key file;` points every node at the same key material. Doing that safely means treating those keys as secrets and rotating them — nginx accepts multiple key files, using the first to encrypt and the rest to decrypt, so a rotation can retire an old key without invalidating tickets already in flight. Never-rotated ticket keys are the standard criticism of tickets: they undercut the forward secrecy the ephemeral key exchange was there to provide. The shared-memory cache has the same multi-node limitation and no equivalent fix — shared memory is shared between workers on one host, not between hosts. ## Diagnosing rather than guessing nginx exposes `$ssl_session_reused` ("r" for reused, "." otherwise) in the log format, which turns this into a measurement: ```nginx log_format tls '$remote_addr $ssl_protocol $ssl_session_reused $request'; ``` Count the ratio over a busy period. A near-zero reuse rate with tickets enabled points at multi-node key mismatch or at clients that disable tickets; a low rate with tickets off points straight at `ssl_session_cache none`. From the client side, `openssl s_client -connect host:443 -reconnect` performs several connections and reports whether the session was reused. ## Where the boundary of this tuning lies Resumption reduces handshake cost; it does nothing for the per-request cost of proxying itself, and it is not a substitute for keeping connections alive in the first place. If the real problem is that a client opens a new connection per request, fix that — resumption merely makes the repeated setup cheaper.

  • Three nginx nodes sit behind an L4 load balancer and resumption almost never succeeds. What is the cause?
    Each node generated its own random session ticket key at startup and keeps its own shared-memory cache, so state created on one node is meaningless on the others. Distribute the same `ssl_session_ticket_key` files to every node, encrypting with the newest and keeping previous ones for decryption, and rotate them on a schedule.
  • What is the security argument against a very long ssl_session_timeout and never-rotated ticket keys?
    Both extend the life of secrets that can resume or decrypt past sessions, undercutting the forward secrecy the ephemeral key exchange provides. A stolen ticket key retroactively covers every ticket it encrypted. Keep resumption lifetimes to hours or a day and rotate ticket keys regularly, accepting a small hit-rate cost.
  • What is the difference between `ssl_session_cache none` and `ssl_session_cache off` in nginx?
    With `none`, nginx tells clients sessions may be reused but stores nothing, so every resumption attempt quietly becomes a full handshake. With `off`, nginx states explicitly that sessions may not be reused, so clients do not try. Both mean no stateful resumption; `none` merely avoids upsetting clients that dislike an outright refusal.

saying these in an interview costs you the question

  • nginx caches TLS sessions by default
  • builtin and shared caches behave the same across workers
  • Session tickets work across nodes automatically
  • Resumption skips certificate validation entirely
  • A longer session timeout has no downside

context