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.
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.
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.
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.
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.
Connection: keep-alive).finally/context-manager release) looks identical to legitimate pool exhaustion from the outside — both present as growing wait times — but they need different fixes: exhaustion needs a bigger pool or less load; a leak needs a code fix.# 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;"
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.
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.
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.