Skip to content
Connection Pooling: Why Opening a New Connection Every Time Is Terrible 3 min

Connection Pooling: Why Opening a New Connection Every Time Is Terrible

SD
ScaleDojo
May 11, 2026
3 min read828 words
Connection Pooling: Why Opening a New Connection Every Time Is Terrible

The Day Django Killed a Production Database

In 2019, a Python Django service at a mid-size fintech handled 200 requests per second. Every request opened a new PostgreSQL connection, ran a query, and closed it. At 200 req/s, that was 200 TCP handshakes, 200 TLS negotiations, 200 authentication rounds - every single second. The database's max_connections was set to 100. When traffic hit 300 req/s during a marketing campaign, PostgreSQL started refusing connections. The entire platform went down for 45 minutes.

The fix took 10 minutes: add a connection pool with 20 connections. Those 20 pooled connections handled 3,000 req/s with room to spare. The team later said it was the highest ROI change they ever made.

The True Cost of a New Connection

What happens when you open a new database connection:

  App Server                           Database Server
      |                                      |
      |--- TCP SYN --------------------------->|  }
      |<-- TCP SYN-ACK -----------------------|  } TCP 3-way handshake
      |--- TCP ACK --------------------------->|  }  ~1 RTT (0.5-5ms)
      |                                      |
      |--- TLS ClientHello ------------------->|  }
      |<-- TLS ServerHello + Cert ------------|  } TLS negotiation
      |--- TLS Key Exchange ------------------>|  }  ~2 RTTs (1-10ms)
      |<-- TLS Finished ----------------------|  }
      |                                      |
      |--- Auth (username/password) ---------->|  }
      |<-- Auth OK + session params -----------|  }  ~1 RTT (0.5-5ms)
      |                                      |
      |--- SELECT * FROM users WHERE id=42 --->|  } Actual query
      |<-- Result row ------------------------|  }  ~2ms
      |                                      |
  Total: 15-25ms overhead for a 2ms query!
  At 1000 req/s = 15,000-25,000ms wasted PER SECOND

How Connection Pooling Works

Without Pool (per-request connections):

  Request 1: [Connect 15ms] [Query 2ms] [Close]  = 17ms
  Request 2: [Connect 15ms] [Query 2ms] [Close]  = 17ms
  Request 3: [Connect 15ms] [Query 2ms] [Close]  = 17ms
  Total: 51ms, 3 connections created and destroyed

With Pool (reused connections):

  Startup:   [Connect 15ms] [Connect 15ms]        Pool ready (2 conns)
  Request 1: [Borrow <0.1ms] [Query 2ms] [Return]  = 2.1ms
  Request 2: [Borrow <0.1ms] [Query 2ms] [Return]  = 2.1ms
  Request 3: [Borrow <0.1ms] [Query 2ms] [Return]  = 2.1ms
  Total: 6.3ms, 2 connections reused forever

  Performance gain: 8x faster, 0 connection overhead per request

Pool Sizing: The Most Misunderstood Setting

Most developers set pool size = expected concurrent users. This is wrong. The PostgreSQL wiki formula: optimal connections = (2 x CPU cores) + number_of_disks. For a 4-core database with 1 SSD, that is 9 connections. Not 100. Not 500. Nine.

Pool Sizing Guide:

  Database Server    Cores   Disks   Optimal Pool   Common Mistake
  ---------------   -----   -----   ------------   ---------------
  Small (t3.medium)    2       1       5              50
  Medium (m5.xlarge)   4       1       9              100
  Large (r5.2xlarge)   8       1      17              200
  XL (r5.4xlarge)     16       1      33              500

  With 5 app instances sharing 1 database:
    Per-instance pool = Optimal Pool / Number of Instances
    Example: 17 total / 5 instances = 3-4 per instance

  Why fewer is better:
  - Each connection = ~10MB RAM on PostgreSQL
  - 500 connections = 5GB just for connection overhead
  - More connections = more CPU context switching
  - More lock contention on shared data structures
  - HikariCP benchmarks: 50 connections performed WORSE than 10

Connection Pool Architecture

Application-Level Pool (HikariCP, SQLAlchemy, c3p0):

  App Instance 1             App Instance 2
  [Pool: 5 conns]            [Pool: 5 conns]
       |||||                      |||||
       vvvvv                      vvvvv
  ============== Database (max_connections=100) ==============

  Problem: 20 app instances x 5 conns = 100 connections
           Add instance 21 = connection refused!

External Pool (PgBouncer, ProxySQL, RDS Proxy):

  App Instance 1   App Instance 2   App Instance 3
  [5 conns]        [5 conns]        [5 conns]
       \\\\\           |||||           /////
        \\\\\          |||||          /////
  =============================================
  |  PgBouncer (multiplexer)                 |
  |  Client connections: 500 (lightweight)   |
  |  Server connections: 20 (real PostgreSQL)|
  =============================================
               ||||||||||||||||||||
  ============== Database (max_connections=100) ==============

  PgBouncer modes:
  - Session: 1:1 mapping (least benefit)
  - Transaction: conn returned after each txn (best for most apps)
  - Statement: conn returned after each statement (breaks multi-stmt txns)

Connection Pool Settings Explained

  • min_size (2-5): minimum connections kept alive even when idle. Avoids cold start latency.

  • max_size (CPU formula): maximum connections. Never exceed database max_connections / number_of_app_instances.

  • idle_timeout (300s): close idle connections after this long. Prevents stale connections.

  • max_lifetime (1800s): close connections after this age regardless. Prevents DNS caching issues and memory leaks.

  • checkout_timeout (5s): how long to wait for a free connection. Fail fast rather than queue indefinitely.

  • validation_query ('SELECT 1'): verify connection is alive before handing it out. Prevents 'connection reset' errors.

Interview Tip

Interviewers love asking 'how would you handle 10K concurrent users with one database?' The answer involves connection pooling with the PostgreSQL formula (2 x cores + disks), plus an external pooler like PgBouncer in transaction mode for multiplexing. Mention that more connections actually hurt performance due to context switching - cite the HikariCP benchmark showing 50 connections being slower than 10. If asked about microservices, explain why PgBouncer is essential: 20 services x 10 connections = 200 connections vs PgBouncer multiplexing 200 app connections into 20 database connections.

Key Takeaway

Connection pooling eliminates the repeated cost of establishing connections. Size your pool based on database capacity (not request volume) using the formula: 2 x CPU cores + disks. A few well-managed connections outperform hundreds of poorly managed ones. Use PgBouncer for multi-service architectures. Every production application should use connection pooling.

Enjoyed this article?

Share it with your network to help others level up their system design skills.

Discussion0

Join the Discussion

Sign in to leave comments, reply to others, or like insights.

Sign In to ScaleDojo

No comments yet. Be the first to start the thread!

Related Articles

Enjoyed this content?

Your support keeps us creating free resources

We put a lot of hours into researching and writing these guides. If it helped you, consider buying us a coffee. Every bit goes toward keeping ScaleDojo's content free and growing.

$

One-time payment via Stripe. ScaleDojo account required.

ScaleDojo Logo
Initializing ScaleDojo