Opening a database connection is expensive
TCP handshake, TLS negotiation, authentication, memory allocation. At high concurrency, paying that cost on every request kills your DB. A connection pool pays that cost once at startup and reuses connections across thousands of requests.
The Cost of Opening a Database Connection#
The naive approach#
Every time a user makes a request, your app needs to talk to the database. The simplest approach: open a fresh connection, run the query, close the connection. Repeat for every request.
At 10 requests per second — annoying overhead but manageable. At 10,000 requests per second — your database collapses. To understand why, you need to know exactly what happens every time you open a connection.
Step 1 — TCP Three-Way Handshake#
Before your app can send a single byte to the database, it needs to establish a TCP connection. This is a three-message exchange:
sequenceDiagram
participant App
participant DB
App->>DB: SYN (I want to connect)
DB->>App: SYN-ACK (Got it, I want to connect too)
App->>DB: ACK (We're connected) Three messages, three network round trips. On a local network each round trip is ~1ms.
Cost: ~3ms, before a single query runs.
Step 2 — TLS Handshake#
If the connection is encrypted (and it should be), the app and DB now need to agree on how to encrypt traffic. This is a separate exchange on top of the TCP connection.
sequenceDiagram
participant App
participant DB
App->>DB: Client Hello (supported algorithms + random number)
DB->>App: Server Hello (chosen algorithm + certificate + random number)
Note over App: Verifies certificate
Note over App,DB: Both independently compute session key from the two random numbers
App->>DB: Encrypted data flows from here The clever part: the session key is never transmitted. Both sides compute it independently using the two random numbers and the agreed algorithm. An eavesdropper who sees all the messages still cannot compute the key.
TLS 1.3 (modern) adds 1 extra round trip. TLS 1.2 (older) added 2.
Cost: ~1-2ms on top of TCP.
Step 3 — Database Authentication#
With a secure channel established, the app now needs to prove to the database who it is.
sequenceDiagram
participant App
participant DB
App->>DB: Username + password (encrypted)
Note over DB: Looks up user in internal auth table
Note over DB: Checks password hash
Note over DB: Loads permissions
DB->>App: Authenticated — here are your access rights The DB has to do a lookup in its own internal user/permissions table. Another round trip, another ~1ms.
Cost: ~1ms.
Step 4 — Memory Allocation#
This is the hidden cost most people miss.
Postgres uses a one-process-per-connection model. When a connection is opened, Postgres forks a dedicated OS process for it. Not a thread — a full process. That process needs its own:
- Stack memory
- Query execution buffers
- Sort and hash working memory (
work_mem) - Local cache of catalog info (table schemas, indexes, permissions)
Per connection memory:
Stack + buffers: ~1-2MB
work_mem (default): ~4MB
Catalog cache: ~1-2MB
─────────────────────────
Total: ~5-10MB per connection
At 1,000 concurrent connections:
1,000 × 8MB = 8GB RAM
→ just for connection overhead
→ before storing a single row of data
→ before running a single query
The full cost summary#
sequenceDiagram
participant App
participant DB
App->>DB: SYN
DB->>App: SYN-ACK (TCP: ~3ms)
App->>DB: ACK
App->>DB: Client Hello
DB->>App: Server Hello (TLS: ~2ms)
App->>DB: Auth request
DB->>App: Auth OK (Auth: ~1ms)
Note over DB: Allocates ~8MB RAM for this connection
App->>DB: SELECT * FROM ...
DB->>App: Query result (finally, actual work) Total overhead per fresh connection: 6-10ms + 5-10MB RAM
At 10,000 RPS with fresh connections:
10,000 × 8MB = 80GB RAM just for connections
10,000 × 8ms = your DB spending all its time handshaking, not querying
The real problem
It's not just the latency. Each Postgres connection is a full OS process. At 10,000 connections, the OS is context-switching between 10,000 processes. That overhead alone degrades query performance — the CPUs are busy managing processes instead of running queries.