Hermes Wiki
Developer/Networking/SessionLayer-ConnectionLifecycle/Fundamentals/connection-reuse-pooling-and-tls-session-resumption

Connection Reuse, Pooling, and TLS Session Resumption

Concept

OSI layer 5 (session) sits above the transport layer's raw byte stream and below the application data itself — its job is managing the lifecycle of a connection: establishing it, keeping it alive across multiple exchanges, and tearing it down. The reason this layer earns its own line item in a performance discussion is that establishing a connection from scratch is one of the most expensive things a network call does, and most systems talk to the same handful of peers (a database, a small set of internal services, a few external APIs) repeatedly enough that paying setup cost on every single call is pure waste.

A plaintext TCP connection needs a three-way handshake (SYN, SYN-ACK, ACK) before a single byte of application data moves — one full round-trip before anything useful happens. Layer TLS on top and the cost compounds: a full TLS 1.2 handshake adds two more round-trips (client hello/server hello with certificate exchange, then key exchange), and TLS 1.3 improved this to one round-trip for a fresh connection — but a fresh connection still means a real network round-trip spent purely on setup, before the request itself is sent. On a connection with 50ms of latency, that's 100-150ms lost to handshaking alone, dwarfing many actual request-processing times.

Connection pooling avoids paying that cost per-request by keeping a set of already-established connections to a target open and idle between uses, and handing one back out to the next request that needs it instead of opening a new one. This matters most for database drivers (opening a fresh Postgres/MySQL connection involves TCP setup, optionally TLS setup, then the database's own auth handshake — all before the first query runs) and for outbound HTTP clients calling the same API repeatedly.

HTTP keep-alive is the same idea at the HTTP layer: instead of closing the TCP connection after every request/response pair (the HTTP/1.0 default), the connection stays open so the next request to the same host reuses it. HTTP/1.1 made keep-alive the default behavior.

TLS session resumption solves a narrower version of the same problem for connections that do get re-established (a client reconnecting after a period, or a new connection to the same server minutes later): rather than redo the full asymmetric-crypto handshake, the client presents a session ticket or session ID from the previous handshake, and the server — if it still recognizes it — resumes with a shortened handshake using previously-negotiated key material. This is why a 0-RTT or single-round-trip reconnect is possible even without keeping the original TCP connection alive: it's the cryptographic state being resumed, not the transport connection itself.

Tradeoffs

Approach Latency win Resource cost Failure mode
New connection per request None — pays full setup cost every time Lowest per-request server state, but highest aggregate connection-churn cost Never stale, but consistently slow under repeated calls
Connection pooling Full handshake cost paid once per pooled connection, amortized across many requests Server/DB must hold open idle connections; pool sizing becomes a real tuning problem A pool sized too small serializes requests behind available connections; sized too large exhausts the target's max-connections limit
HTTP keep-alive Saves TCP (and TLS, if applicable) setup on every subsequent request to the same host Server holds the socket open during idle time between requests An idle timeout that's too short defeats the purpose; too long ties up server file descriptors
TLS session resumption Cuts handshake round-trips even on a genuinely new connection Server must cache session state (or use encrypted session tickets to stay stateless) A session ticket rotated or expired forces a fallback to a full handshake — not a correctness bug, just a lost optimization for that one reconnect

The underlying trade is always the same: paying setup cost once and holding state open (pool, keep-alive socket, cached session) versus paying it fresh every time and holding no state. Nearly every real system with repeat traffic to the same peer lands on the "hold state open" side — the exceptions are truly one-off connections where the state would just be dead weight.

When to use / when not to

  • Always pool database connections — a fresh DB connection pays TCP setup, optional TLS, and the database's own authentication handshake before the first query even runs; a pool amortizes all of that across every query the application ever makes.
  • Enable and rely on HTTP keep-alive for any outbound client that calls the same host more than once (this is the default in essentially every modern HTTP client library — the risk is usually accidentally disabling it, not forgetting to enable it).
  • Rely on TLS session resumption where the runtime/library supports it automatically (most modern TLS stacks do) — there's rarely a reason to disable it.
  • Skip pooling for connections that are genuinely one-shot with no realistic reuse (a script that makes a single call and exits) — the pool's idle-connection bookkeeping is pure overhead with nothing to amortize it against.
  • Watch pool sizing on both ends: a client-side pool bigger than the server's max-connections limit just moves the bottleneck to the server refusing connections instead of the client waiting on its own pool.

Common pitfall

Sizing a connection pool by guesswork (a round number like 10 or 100) instead of by the target's actual concurrency limits and the application's actual request concurrency — a pool too small serializes requests behind a handful of connections and shows up as latency that looks like "the database is slow" when the database itself is idle; a pool too large exhausts the target's own connection ceiling (Postgres's default max_connections is often 100) and causes other clients sharing that database to get connection-refused errors during a load spike.

Engineering Lens

Connection lifecycle sits right next to the transport layer's own handshake cost (TCP vs. UDP) and is one of the cheapest latency wins available precisely because it requires no algorithmic change to the application — just not throwing away already-paid-for setup work. The judgment call that matters in a design review isn't "do we pool connections" (the answer is almost always yes) but naming the actual concurrency ceiling on the other end of the pool and sizing to it deliberately, the same way circuit breaker thresholds need to be tuned against real dependency behavior rather than left at a library default.

Sources

Hermes Wiki