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