← Computer Networks
Traffic Management & Resilience

Connection Pooling & Keep-Alive

Watch: "Connection Pooling in PostgresSQL with NodeJS (Performance Numbers)" — Hussein Nasser

Opening a fresh connection for every request is one of the most common accidental bottlenecks in backend systems — it hides in plain sight until load testing reveals it. Without reuse, every single request pays: new TCP connection → do the work → close, over and over. Understanding handshake cost and reuse strategies is essential for reasoning about both HTTP and database latency, and pool-sizing questions ("what happens if it's too small / too large") are a near-guaranteed interview follow-up.

What Is Connection Pooling?

Connection pooling is keeping a small set of ready-to-use connections open and sharing them between requests, instead of creating a brand new connection every single time one is needed. A request borrows an already-open connection, uses it, and returns it to the pool for the next request to reuse.

What Is Keep-Alive?

Keep-alive is a setting that tells two computers talking over the internet not to hang up after just one exchange. Instead of closing the connection the moment a request is answered, both sides keep it open briefly in case another request follows right after, so the next exchange can skip the setup step entirely.

Where This Is Used

A Day-to-Day Example

Picture a bank with a line of tellers. Instead of hiring and training a brand-new teller for every single customer and then letting them go right after, the bank keeps a fixed team of tellers on staff, and each free teller simply serves the next customer in line. Keep-alive is like a customer with several errands staying at the same teller's window instead of going to the back of the line each time; connection pooling is the bank managing that fixed team so there are always enough tellers, but not so many that most of them sit idle all day.

Deep Dive: The Cost You're Actually Paying Per Connection

Connection per request versus a reused connectionnew connection each timeTCPTLSreq 1TCPTLSreq 2TCPTLSreq 3keep-alive / poolTCPTLSreq 1req 2req 3handshake paid onceeach block ≈ one round trip: 3 requests cost 9 vs 5
Without reuse, every request pays for a TCP handshake and a TLS handshake before any data moves. Keep-alive and connection pools pay that once and then send request after request on the same connection.
handshakeuseful request
Show text version
No reuse (new connection every request):
  [SYN]->
  <-[SYN-ACK]      1 RTT for TCP handshake
  [ACK]->
  [TLS ClientHello]->
  <-[ServerHello, cert]   1-2 more RTTs for TLS handshake (less with
  [Finished]->              TLS 1.3's 1-RTT / 0-RTT resumption)
  [ HTTP request ]->
  <-[ HTTP response ]
  [ connection closed ]
  ---- repeat all of the above for the NEXT request ----

With keep-alive / pooling (handshake paid once, reused N times):
  [ TCP+TLS handshake: paid ONCE ]
  [ request 1 ]->  <-[ response 1 ]
  [ request 2 ]->  <-[ response 2 ]
  [ request 3 ]->  <-[ response 3 ]   (same connection, no re-handshake)
  ...

On a connection with ~50ms RTT to the peer, that's roughly 50ms for the TCP handshake plus another 50-100ms for TLS — meaning without reuse you could be spending more time handshaking than actually processing the request. Multiply that by thousands of requests/sec and it's real, measurable CPU and latency, not a rounding error.

Deep Dive: Pool Sizing — the Question That Always Follows

Key Pointers

How It Works

  1. On the first request, the client performs a TCP handshake (plus a TLS handshake if secure) to establish a connection.
  2. The connection is kept open — via HTTP keep-alive or checked into a pool — rather than closed after one use.
  3. Subsequent requests or queries reuse the existing connection, skipping handshake cost entirely.
  4. A pool manager tracks in-use versus idle connections, enforcing minimum/maximum pool size and idle timeouts.
  5. When a caller needs a connection, it borrows one from the pool — or waits/queues if all are busy — and returns it when finished, making it available for the next borrower.

Try It Yourself

# Confirm keep-alive is active and see the connection get reused —
# verbose curl shows whether a new connection was opened
curl -v https://example.com/ 2>&1 | grep -i "connect\|connection"

# Issue two requests on the SAME connection and observe there is
# only ONE TCP/TLS handshake for both
curl -v https://example.com/ https://example.com/about 2>&1 | grep -i "Re-using\|Connected to"

# Time a cold request vs a warm (reused-connection) request
curl -w "connect: %{time_connect}s appconnect: %{time_appconnect}s total: %{time_total}s\n" -o /dev/null -s https://example.com/
curl -w "connect: %{time_connect}s appconnect: %{time_appconnect}s total: %{time_total}s\n" -o /dev/null -s https://example.com/  # second call, likely reused

# Load-test and watch connection reuse under concurrency
hey -z 15s -c 100 https://example.com/

# Postgres: see actual open connections and compare to your pool's
# max size / the database's max_connections setting
psql -c "SELECT count(*) FROM pg_stat_activity;"
psql -c "SHOW max_connections;"

Interview Q&A

Q: What happens if the connection pool is too small?

A: Requests start queuing, waiting for a connection to free up. Under moderate load this just adds latency; under heavy load, that wait time can exceed the caller's own timeout, turning into outright request failures — and the failure looks like a downstream problem even though the actual bottleneck is the pool's artificial ceiling, not the database or backend itself.

Q: What happens if it's too large?

A: You risk overwhelming the backend with more concurrent connections than it can handle well — for a database specifically, each connection consumes memory (and often a dedicated backend process), so an oversized pool can exhaust the database's resources or hit its hard max_connections limit, potentially starving other services that share the same database. It also just wastes memory holding idle connections that provide no benefit.

Q: How do you pick the right pool size in practice?

A: Start from the downstream's actual capacity (e.g. database max_connections divided across the number of app instances that connect to it), then load-test and watch p99 latency and queue-wait metrics as you vary the size — the right size is the smallest one that keeps queue-wait near zero under expected peak load. It's an empirical tuning exercise, not a fixed formula, and it needs revisiting whenever instance count or traffic pattern changes.

System Design Tie-in

This comes up any time a design has a service talking to a database, cache, or another internal service under real load — interviewers expect you to mention connection pooling as a given rather than something exotic, and to know that pool size needs to be planned relative to the downstream's connection limits, not just picked arbitrarily large "to be safe." It's also directly relevant when discussing horizontal scaling: if you scale from 10 to 100 app instances and each holds a pool of 20 database connections, you've just asked the database to support 2,000 connections — a classic way naive horizontal scaling breaks a database, and exactly the scenario a pooler like PgBouncer is designed to solve.

Trade-offs

Pooling trades a bit of memory and idle-connection overhead for much lower per-request latency, which is almost always the right trade at scale. Undersized pools cause contention and timeouts under load; oversized pools waste database resources and can hit hard connection limits. Interview gotcha: distinguish pool exhaustion (every connection legitimately busy, causing queuing) from a connection leak (code that borrows a connection but never returns it) — both present as latency spikes or timeouts, but they require different fixes.