Vasu Patel logoVasu Patel
← Built to Last: Reliability & Scaling
All chapters in this track
  1. 01Chapter 1: Why Reliability and Scale Matter
  2. 02Chapter 2: Reliability Vocabulary: SLAs, SLOs, SLIs, and Error Budgets
  3. 03Chapter 3: Redundancy and Fault Tolerance: Eliminating Single Points of Failure
  4. 04Chapter 4: Horizontal vs. Vertical Scaling, and Load Balancing/Auto-Scaling
  5. 05Chapter 5: Caching for Scale: Cache Stampede and Invalidation at Scale
  6. 06Chapter 6: Database Scaling: Replication, Partitioning, Read Replicas, Connection Pooling
  7. 07Chapter 7: Resilience Patterns: Timeouts, Retries, Circuit Breakers, Bulkheads, Load Shedding
  8. 08Chapter 8: Observability: Metrics, Logging, Distributed Tracing, and Actionable Alerts
  9. 09Chapter 9: Incident Response: Severity, Incident Command, Communication, Blameless Postmortems
  10. 10Chapter 10: Chaos Engineering: Proactively Testing for Failure
  11. 11Chapter 11: Capacity Planning and Load Testing
  12. 12Chapter 12: Core Trade-offs: Consistency vs. Availability, Cost vs. Redundancy, Simplicity vs. Resilience
  13. 13Chapter 13: Incident Case Study: The Cascading Failure
  14. 14Chapter 14: Incident Case Study: The Silent Data Corruption
  15. 15Chapter 15: Common Interview Questions About Reliability and Scaling

Chapter 6 of 15

Chapter 6: Database Scaling: Replication, Partitioning, Read Replicas, Connection Pooling

Why this matters

Replication and partitioning/sharding are covered in full depth in the companion Database book (Chapters 10-11) and applied at the systems level in the companion System Design book (Chapter 7) — this chapter's specific, additive contribution is connection pooling, a topic neither of those books covered in depth, and one that causes a genuinely common, specific, easily-preventable category of production reliability incident.

Quick recap: replication and partitioning as reliability tools

[BEGINNER] Replication (Database book, Chapter 10) provides both read scaling and availability (a replica can be promoted if the primary fails) — directly a Chapter 3 redundancy technique, applied to the database layer specifically. Partitioning/sharding (Database book, Chapter 11) scales beyond a single node's capacity — but as System Design book Chapter 7 emphasized, this should only be adopted once genuinely justified by actual, measured load, not preemptively.

Connection pooling: the specific, common reliability problem

[SENIOR INSIGHT] Every database connection has real, non-trivial overhead to establish (a TCP handshake, per the companion Networking book's Chapter 3, plus authentication and session setup) — and every database has a hard limit on how many concurrent connections it can maintain at all. A naive application that opens a new database connection for every single request, rather than reusing a pool of already-established connections, hits two real problems simultaneously: wasted overhead (paying connection-setup cost on every single request) and, far more seriously, a genuine reliability risk: under enough concurrent load, the application can exhaust the database's maximum connection limit entirely, causing every subsequent connection attempt — including from completely unrelated parts of the application — to fail outright.

Without connection pooling: EVERY request opens a NEW database
connection, uses it once, and closes it. Under load, hundreds of
concurrent requests can mean HUNDREDS of simultaneous connection
attempts, easily exceeding a typical database's max_connections limit
(often in the hundreds by default) -- once exceeded, NEW connection
attempts fail outright, even for otherwise-healthy, otherwise-capable
requests, causing a cascading, self-inflicted outage.

With connection pooling: a FIXED, bounded number of connections
(e.g., 20) are established ONCE and REUSED across many sequential
requests -- overhead is paid once per connection, not once per
request, and the database's connection limit is never at risk of
being exceeded regardless of how much concurrent request volume arrives.
# Conceptual connection pool usage
from psycopg2 import pool

connection_pool = pool.SimpleConnectionPool(minconn=5, maxconn=20, dsn="...")

def handle_request():
    conn = connection_pool.getconn()  # borrow an existing connection
    try:
        # ... use conn for this request's queries ...
        pass
    finally:
        connection_pool.putconn(conn)  # return it to the pool, NOT closed

Sizing a connection pool correctly: a real, non-obvious trade-off

[IN PRACTICE] A pool sized too small becomes its own bottleneck — requests queue up waiting for an available connection, adding latency even though the database itself has spare capacity. A pool sized too large risks the exact connection-exhaustion problem pooling exists to prevent, especially when multiple application server instances (Chapter 4's horizontal scaling) each maintain their own pool — the database's actual total connection load is the sum across every single instance's pool, not just one instance's pool size in isolation, a detail that's easy to overlook when scaling out the number of application instances without recalculating the aggregate connection demand.

5 application instances x a pool of 20 connections each = 100 TOTAL
connections to the database -- if the database's max_connections is
120, this leaves little headroom for anything else (admin connections,
monitoring tools, a batch job) -- a connection pool sizing decision
made per-instance without accounting for the FLEET-wide total is a
common, easily-overlooked reliability risk specifically when horizontal
scaling (Chapter 4) increases instance count.

Common mistakes

  1. Opening a new database connection per request instead of using a connection pool, incurring both unnecessary overhead and a genuine risk of exhausting the database's connection limit under real load.
  2. Sizing a connection pool without accounting for the total number of application instances that will each maintain their own pool, risking fleet-wide connection exhaustion even when each individual instance's pool looks reasonably sized in isolation.
  3. Sizing a pool too small, creating an artificial bottleneck (requests queueing for a connection) even when the database itself has spare capacity to handle more concurrent connections.
  4. Not accounting for non-application connections (monitoring tools, administrative access, batch jobs) when calculating how much of the database's total connection limit is actually available for the application's own pools.

Discussion / interview questions

  1. "Why does opening a new database connection for every request create a reliability risk, not just a performance inefficiency?" Strong answer: Every database has a hard limit on concurrent connections; under enough load, opening a new connection per request can exhaust that limit entirely, causing subsequent connection attempts to fail outright — a self-inflicted, cascading outage risk, not just wasted per-request overhead.

  2. "How would you correctly size a connection pool across a horizontally-scaled fleet of application instances?" Strong answer: Calculate the total connection demand across ALL instances (pool size × instance count), not just one instance's pool size in isolation, and ensure that fleet-wide total stays comfortably within the database's max connection limit, leaving headroom for non-application connections (monitoring, admin access, batch jobs).

  3. "What happens if a connection pool is sized too small for actual demand?" Strong answer: Requests queue up waiting for an available connection from the pool, adding latency even if the database itself has spare capacity to handle more concurrent connections — an artificial, avoidable bottleneck caused by pool sizing rather than actual database capacity.

  4. "How does horizontal scaling (adding more application instances) interact with database connection pool sizing?" Strong answer: Each new instance typically maintains its own connection pool, so the database's total connection load grows with instance count even if each instance's individual pool size stays the same — a scaling decision made without recalculating the aggregate connection demand can silently push the fleet toward the database's connection limit.

Chapter summary / cheat sheet

  • Replication (read scaling + availability) and partitioning (beyond-single-node capacity) are covered in depth in the companion Database and System Design books — both are, respectively, a redundancy technique and a scale-driven, evidence-justified decision.
  • Connection pooling reuses a fixed, bounded set of established database connections across many requests, avoiding both per-request connection overhead and the genuine risk of exhausting the database's hard connection limit under load.
  • Size a connection pool accounting for the TOTAL across every application instance in a horizontally-scaled fleet, not just one instance's pool size in isolation — this fleet-wide total is what actually determines proximity to the database's connection limit.
  • A pool sized too small creates an artificial latency bottleneck; sized too large (especially fleet-wide) risks the exact connection exhaustion pooling exists to prevent.