After this lesson, you will be able to: Apply read replicas, connection pooling, sharding, and the question 'do we even need a cache' to scale a database past its single-instance limit.
The database is usually the first thing to break at scale. This lesson covers the standard techniques: read replicas, connection pooling, sharding, and the caching decision that often beats all of them.
A read replica is a copy of the primary database that asynchronously receives writes from the primary. Reads can hit the replica; writes still go to the primary. Multiplies read capacity nearly linearly per replica (1 primary + 4 replicas = ~5x read throughput). Trade-off: replication lag (milliseconds to seconds) means replicas serve slightly stale data. Acceptable for most reads; not for 'just wrote, must read'.
Every database connection costs memory on the DB server. Postgres typically handles 100-300 concurrent connections before performance degrades. If 10 app servers each open 50 connections, you've got 500 connections, which exceeds the budget. Connection poolers (PgBouncer, pgpool, Supabase's built-in pooler, RDS Proxy) sit between your apps and DB, multiplexing many app connections onto fewer DB connections. Connection pooling is the #1 fix when 'the DB is slow' is actually 'we're out of connection slots'.
Sharding partitions the data across multiple databases (shards). User #1-100K go to shard A; #100K-200K go to shard B; etc. Each shard is independent; queries that touch multiple shards become hard (especially joins). Picking a shard key: usually user_id, account_id, or tenant_id. Pick a key with even distribution and high cardinality. Modern alternatives: Vitess (sharded MySQL), Citus (sharded Postgres), CockroachDB (auto-sharded). Sharding is the last resort after read replicas, caching, and connection pooling.
A cache helps when: reads dominate writes; the same data is read many times; the cost of staleness is acceptable. Cache options: in-process (LRU in Node memory; fast but per-instance), Redis (shared across instances, persistent or ephemeral), CDN (for HTTP responses), database-level (Postgres query cache, materialized views). The decision tree: do you need cache invalidation more often than once per hour? If yes, caching adds complexity you should defer. Postgres can handle a LOT of load before caching becomes mandatory.
Real Node.js / Prisma config patterns.
// Prisma + Postgres direct connection (small apps, ~10 connections)// .envDATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=10"// Prisma + Postgres via PgBouncer (production)// PgBouncer in transaction-pool mode is the safe default for PrismaDATABASE_URL="postgresql://user:pass@pgbouncer:6432/db?pgbouncer=true&connection_limit=1"// Supabase (their built-in pooler handles this for you)// Connection pool sizing rule of thumb:// Each app instance: 5-20 connections// Total connections to DB: (instances * per-instance) + headroom for migrations / admin// Should stay well under DB's max_connections setting
Sharding before exhausting read replicas + connection pooling + caching. Sharding is hard; defer it. Caching everything by default. Cache invalidation is one of the two hard problems in CS for a reason. Skipping connection pooling and being surprised when 'the DB is slow'. Routing all reads to replicas without thinking about replication lag. 'Just wrote, must read' needs to go to primary or be tagged for sync replication. Treating ORM-generated SQL as opaque. Read the actual queries; the N+1 query problem is the most common DB bottleneck and it's invisible from the ORM call site.
Pick the most cost-effective fix.
Sign in and purchase access to unlock this lesson.