DB scaling
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 18 min · 5 figures

    Database scaling

    The ladder for scaling a database, one rung at a time: measure, tune queries, scale up, pool connections, cache, add replicas, partition, split by domain, add specialised stores, shard, and move to distributed SQL, with the signal, gain and cost of each.

    Database scaling is the set of moves that let one logical database serve more queries and more data than it was built for. The moves form a ladder: measure, tune queries, buy a bigger machine, pool connections, cache, add read replicas, partition, split by domain, add specialised stores, shard, and finally adopt a database that shards itself. Each rung buys capacity at a price, and the price rises as you climb, so you take the next rung only when measurements say the current one has run out.

    Picture it first: one kitchen at dinner rush

    A popular restaurant has one kitchen and one head chef who writes every order into the order book. At 8 pm the queue reaches the street. The owner could open three branches tomorrow, but a sensible owner first stands in the kitchen with a stopwatch.

    The fixes come in order of cost: rewrite the two slow recipes, buy a bigger stove, have a host seat diners in waves, keep popular desserts ready on a counter, put copies of the order board at side counters, move the bakery and the bar into their own rooms. Only when the kitchen itself is full do new branches, each owning a set of customers, make sense.

    Map it back: the stopwatch is monitoring, recipes are queries and indexes, the stove is the instance size, the host is a connection pool, the dessert counter is a cache, side counters are read replicas, separate rooms are per-domain databases, and branches are shards.

    The ladder at a glance

    The scaling ladder. Each rung is harder to build and harder to undo than the one below it; the brackets show what each group of rungs actually scales.Notice Only the top group splits a single dataset across machines; the rungs below make one primary go further, copy it, or move whole tables elsewhere.

    Three principles keep the climb sane:

    • Climb on evidence. Each rung answers a specific bottleneck; a cache does nothing for a write-bound primary.
    • Exhaust cheap rungs first. Rungs 0 to 4 need little application change and are easy to reverse. Rungs 7 to 10 change your data model, failure modes and often team structure.
    • Start the next rung early. A functional split or shard migration takes months; begin while the current rung still has headroom.

    Reads and writes are different problems

    Most scaling confusion disappears once you separate reads from writes. A read can be answered by any up-to-date copy of the data, so you scale reads by making more copies: caches and replicas. A write must be applied to every copy, so copies never reduce write work; they multiply it. You scale writes only by splitting the data so that each machine owns a fraction of it.

    The same load, W writes and R reads a second, on three replicas versus three shards.Notice Every replica still applies all W writes; only the shards divide them.

    So the first question about any scaling problem is the ratio. A feed or a catalogue might serve 100 reads per write; a payments ledger or an IoT ingest pipeline might be close to 1 : 1. Read-heavy systems can go a very long way on rungs 0 to 5. Write-heavy systems reach the top of the ladder sooner. The patterns for each side are collected in scaling reads and scaling writes.

    Rung 0: measure before you climb

    Every rung fixes one kind of saturation, so first find which resource runs out at peak, and which query shapes cause it.

    Resource What to watch What saturation looks like
    CPU CPU at peak; total time per query shape in pg_stat_statements or the MySQL performance_schema Latency climbs with load; a few query shapes dominate total time
    Memory Buffer cache hit ratio; working set against RAM Reads start going to disk and IOPS climb with traffic
    Storage I/O IOPS and throughput against the volume’s limits; I/O latency A flat ceiling on IOPS with rising latency
    Locks Wait events, lock waits, deadlocks CPU is idle but queries are slow
    Connections Active and idle connections against the limit Connection errors, memory spent on idle sessions
    Replication Replica lag Stale reads; a failover would lose more data
    Storage size Growth per month, bloat Months left before the volume limit; backups and restores take hours

    Set a headroom target rather than waiting for 100%, because latency turns unpredictable well before saturation; Figma acted when its primary passed 65% CPU at peak. Put the numbers on dashboards (observability) and project them forward: at 3× growth a year, 30% CPU today is 90% in twelve months.

    Rungs 1–4: make one primary go further

    These four rungs keep a single primary and make each unit of its capacity do more. None of them moves data between servers.

    1. Tune queries and indexes

    Signal: a few query shapes dominate total database time; plans show sequential scans over big tables, huge sorts, or one query issued a hundred times per page.

    Gain: usually the biggest improvement per hour of work, with no new infrastructure. Cost: engineering time, plus a write cost for every index you add.

    Rank queries by total time, read their execution plans, and add or fix indexes (in practice). Hunt for the N+1 pattern, deep OFFSET pagination and ORM-generated joins: OpenAI traced past severe incidents to one query joining 12 tables.

    2. Scale up the primary

    Signal: CPU or memory saturated after tuning; the working set no longer fits in RAM; the volume’s IOPS ceiling is reached.

    Gain: more of everything with no code change. Cost: the price, one machine as one failure domain, and a hard ceiling.

    The ceiling is high. As of October 2026, the largest Amazon RDS classes for PostgreSQL and MySQL offer 192 vCPUs and 1,536 GiB of memory (db.r8g.48xlarge, db.r7i.48xlarge), or 128 vCPUs and 4,096 GiB for memory-hungry working sets (db.x2iedn.32xlarge). An RDS instance holds up to 64 TiB, and io2 Block Express volumes reach 256,000 IOPS. Aurora’s shared cluster volume grows to 128 TiB, or 256 TiB on recent engine versions.

    Scaling up cannot fix contention on one hot row, a noisy neighbour workload, or the fact that every failover still takes the whole database with it.

    Scaling up versus scaling out, as cost against capacity. The shape is illustrative, not real prices; the wall is the largest instance you can buy.Notice The move off the scale-up line has to start well before the wall, while there is still headroom to run a migration.

    The scale-out line starts higher because splitting has a fixed cost: routing, more replicas, more on-call surface and the engineering time to build it. Below the wall, scaling up is almost always cheaper; vertical vs horizontal scaling covers the general trade-off.

    3. Pool connections

    Signal: connection counts near the limit, many idle connections, connection storms when the app tier autoscales, or memory consumed per session.

    Gain: the database runs a small, steady number of busy connections however many app instances exist. Cost: a pooler to run, and transaction-mode pooling breaks session state such as SET, LISTEN and session-level advisory locks.

    PostgreSQL runs a process per connection, so thousands of idle clients cost real memory. OpenAI hit Azure PostgreSQL’s 5,000-connection limit in connection storms; PgBouncer fixed it and cut average connection time from 50 ms to 5 ms. Sizing is in connection pooling.

    4. Cache hot reads

    Signal: the same rows or results are read again and again; reads dominate CPU; you can tolerate a little staleness.

    Gain: most read traffic never reaches the database. Cost: invalidation, stale data, a new dependency, and a dangerous failure mode: when the cache empties, every miss lands on the database at once.

    See caching 101 and caching strategies. OpenAI serves most reads from a cache and uses leases so only one request per missing key reaches PostgreSQL (stampedes). Size the database to survive a cold cache.

    Rung 5: read replicas

    Signal: reads still dominate primary CPU after caching, or users far from the primary need lower read latency.

    Gain: read capacity grows roughly with the number of replicas, and replicas double as failover candidates and regional read copies. Cost: replication lag, more instances to pay for, and no help at all for writes.

    RDS for PostgreSQL and MySQL allows up to 15 read replicas per primary, and an Aurora cluster up to 15 Aurora Replicas, which share the writer’s storage volume, so their lag is usually well under 100 ms. Two costs deserve attention:

    • Lag changes behaviour. A user who updates a profile and reads it back from a replica may see the old value. Route read-your-writes paths to the primary; data replication covers the anomalies and fixes.
    • The primary pays for every replica. It streams its WAL to each one; OpenAI, at nearly 50 replicas, is testing cascading replication so that intermediate replicas relay WAL instead.

    Rungs 6–8: split by table and by workload

    Once one primary is doing only necessary work, the next rungs take work away from it, without yet splitting any single dataset across servers.

    6. Partition big tables

    Signal: one table dwarfs the rest; deleting old rows generates hours of I/O and bloat; vacuum and index builds on it never finish quietly.

    Gain: queries prune to the partitions they need, retention becomes dropping a partition, and maintenance runs per partition. Cost: the partition key must appear in every unique key, and queries without it visit every partition. Partitioning stays inside one server, so it adds no write capacity; the details are in table partitioning.

    7. Split by domain

    Signal: the schema falls into groups of tables that are rarely joined across groups; one feature’s load or migration hurts unrelated features; separate teams own separate tables.

    Gain: each group gets a whole primary of its own, a smaller blast radius and independent scaling. Cost: no joins or transactions across the split; cross-domain consistency moves into the application (sagas), and there are more databases to operate.

    This vertical partitioning, or functional split, is the rung most teams underrate. Figma instrumented its ORM to log which tables each transaction touched, then moved groups of tables that were always used together onto their own databases, aiming for under a minute of availability impact per move. Boundaries usually follow your domains (finding service boundaries, monolith vs microservices), and the anti-pattern it fixes is monolithic persistence.

    8. CQRS and specialised stores

    Signal: queries the primary is bad at: substring and relevance search, analytics scanning months of rows, denormalised feeds, counters.

    Gain: each query shape runs on an engine built for it, and the primary only takes transactional work. Cost: read stores lag the primary by seconds, a pipeline to operate, and the same fact stored in several places.

    The pattern is CQRS: writes go to the system of record, and change data capture or a transactional outbox streams every change to read-optimised stores: a search index such as Elasticsearch, a columnar warehouse, caches and precomputed views.

    Rungs 7 and 8 together: one database per domain on the write side, and a change stream that keeps search, analytics and read models up to date.Notice The read stores never take writes from the app; everything in them arrives through the change stream, so a broken one can be rebuilt from the source of truth.

    Rungs 9–10: split the data itself

    When one domain’s writes, storage or working set outgrow the biggest primary you can buy, the dataset itself has to be divided.

    9. Shard

    Signal: after the cheaper rungs, a single domain still needs more write throughput or storage than one primary provides, and projections show the wall within about a year.

    Gain: writes, storage and working set scale with the number of shards. Cost: a shard key in nearly every query, scatter-gather without it, cross-shard transactions, resharding, and every chore multiplied by N. Concepts are in database sharding; Vitess, Citus and the Instagram, Notion and Figma migrations are in sharding a relational database.

    10. Distributed SQL or managed NoSQL

    Signal: you need horizontal write scale but don’t want to own a sharding layer, or you need strongly consistent writes across regions.

    Gain: the engine splits, rebalances and replicates data for you, keeping SQL and transactions. Cost: consensus latency on writes, compatibility gaps with the PostgreSQL or MySQL you know, a migration, and usually a higher bill.

    • Google Spanner splits data automatically, replicates it synchronously with Paxos and orders transactions with TrueTime clocks; Google advertises up to 99.999% availability, with GoogleSQL and PostgreSQL dialects.
    • CockroachDB breaks tables into ranges, replicates each range three ways by default with Raft, splits ranges as they grow, and exposes a PostgreSQL-compatible SQL API.
    • YugabyteDB (architecture) is PostgreSQL-compatible on a Raft-replicated, sharded storage layer; TiDB (overview) is MySQL-compatible, with a row store (TiKV) and a columnar replica (TiFlash) for analytics.
    • Aurora PostgreSQL Limitless Database keeps one endpoint in front of a shard group of routers and shards; you declare tables as hash-sharded, collocated with another table on the same key, or reference tables copied to every shard.

    Managed NoSQL stores such as DynamoDB and Cassandra scale out from day one, at the price of modelling data around known access patterns with limited joins (Cassandra and DynamoDB).

    Distributed SQL is not free sharding

    You still choose primary keys that spread load, still avoid hot rows, and still pay a network round trip for writes that must reach a majority of replicas. What you stop doing is writing the router, the resharding tool and the cross-shard transaction protocol yourself.

    How far can one primary go?

    Interviewers like a number here. The honest answer is a range you derive, not a benchmark quoted from memory.

    Reads, when they are CPU-bound. Assume a simple indexed lookup on cached pages costs 100 to 200 µs of server CPU (an assumption; measure your own). On the largest 192-vCPU instance at a 60% CPU target, you have 192 × 0.6 ≈ 115 core-seconds per second, so 115 ÷ 0.0002 ≈ 575,000 to 115 ÷ 0.0001 ≈ 1.15 million simple reads per second at best. Real mixes with joins and sorts cost far more, so plan for a fraction of that.

    Writes, when indexes don’t fit in memory. Suppose each insert updates four B-tree indexes on random keys and their leaf pages are usually not cached: about 4 random page reads now and about 4 page writes later, roughly 8 I/Os per insert. At 64,000 IOPS (the top of RDS gp3) that is 64,000 ÷ 8 = 8,000 inserts a second; at io2’s 256,000 IOPS, 32,000. With time-ordered keys and cached indexes, an insert costs little more than its share of sequential WAL, and the limit moves to CPU, which only your own benchmark can measure.

    A worked example. A UPI-style payments service handles 10 million payments a day:

    • Average: 10M ÷ 86,400 ≈ 116 payments a second; plan for a 10× peak, about 1,160 a second.
    • Assume each payment writes about 5 rows (the payment, two ledger entries, a status change, an audit row): 1,160 × 5 ≈ 5,800 row writes a second at peak. That fits under even the uncached-index ceiling of 8,000, but with thin headroom, so keep indexes few and keys time-ordered, and partition the big tables.
    • Assume about 20 reads per payment (status polls, history, balance): about 23,000 reads a second at peak, a job for a cache and a few replicas.
    • At roughly 250 bytes a row plus indexes, the payments table grows about 105 GB a month, about 1.3 TB a year: decades from a 64 TiB limit.

    Most products never outgrow one well-run primary plus replicas and caches. Those that do usually hit write throughput on one domain, one huge table, or operational limits (vacuum, index builds, multi-hour restores) long before raw storage. Practise the arithmetic in back-of-envelope estimation.

    Choosing the next rung

    Rung Signal You gain You pay Complexity
    1 · Tune queries, indexes A few query shapes dominate time; bad plans Often the largest win, no new parts Engineering time; write cost per index Low
    2 · Scale up CPU, RAM or IOPS saturated after tuning Capacity with no code change Price; hard ceiling; one failure domain Low
    3 · Pool connections Connections near the limit; storms on deploy Steady, small set of busy connections A pooler to run; session features limited Low
    4 · Cache Repeated reads of hot keys Most reads skip the database Staleness, invalidation, miss storms Medium
    5 · Read replicas Read CPU high, lag tolerable Read capacity, regional reads, failover Lag anomalies; no write help; WAL fan-out Medium
    6 · Partition One huge table; painful retention deletes Pruning, cheap drops, smaller maintenance Key in unique constraints; no write capacity Medium
    7 · Split by domain Separable table groups; noisy features A primary per domain; smaller blast radius No cross-domain joins or transactions High
    8 · CQRS stores Search, analytics or feeds on the primary The right engine per query shape Lagging read stores; pipelines to run High
    9 · Shard One domain beyond the biggest primary Writes and storage scale with nodes Shard key everywhere; cross-shard pain; resharding Very high
    10 · Distributed SQL Need scale-out without owning sharding Engine shards and rebalances for you Consensus latency; migration; compatibility; cost High, mostly up front
    The scaling ladder: the signal for each rung, what you gain and what you pay

    When you know which resource saturates, the order of questions matters more than the table. Ask about query cost and connections before anything structural, then split the problem into reads and writes.

    A decision flow from the measured bottleneck to the next rung. Each leaf names the rung to try first, not the last one you will ever need.Notice The structural rungs only appear after query cost and connections are ruled out, and that reads and writes lead to different branches.

    Going deeper: real ladders and common mistakes

    Figma: up the ladder in order

    In 2020 most of Figma’s metadata lived in one large RDS PostgreSQL database, above 65% CPU at peak, with traffic tripling yearly. The team bought a year of runway with rungs 2 to 5: the largest instance available (r5.12xlarge to r5.24xlarge), read replicas, new databases for new features, and PgBouncer in front of thousands of connections. Writes were still heavy and some reads were too lag-sensitive for replicas, so, after ruling out NoSQL, Vitess and managed distributed PostgreSQL, it chose rung 7 and moved groups of tables to their own databases. Only when single tables reached terabytes and billions of rows did it shard.

    OpenAI: one primary, very far

    OpenAI’s 2026 write-up describes PostgreSQL serving ChatGPT’s 800 million users from a single primary and nearly 50 read replicas, at millions of queries per second, after load grew more than 10× in a year. The ladder shows in the details: query tuning and timeouts, PgBouncer, a cache with leases, replicas now limited by WAL fan-out, separate instances for high- and low-priority traffic, rate limits per query shape, and schema changes capped at a 5-second timeout. For writes it took the rung-7 and rung-9 route: shardable, write-heavy workloads moved to sharded systems such as Azure Cosmos DB, and new tables are no longer allowed in PostgreSQL. Its one severe PostgreSQL incident that year came from a write surge of more than 10× during a viral launch, exactly where a single primary is weakest.

    In the interview

    Use these stories to anchor your answer: “A single well-run primary with replicas can carry a very large read-heavy product; OpenAI does it for ChatGPT. I’d shard only the domain whose writes outgrow one box, and I’d split by domain before that.”

    Common mistakes

    • Sharding too early. You pay the shard-key tax on every query and run N databases for years before you need them.
    • Caching before indexing. The cache hides a missing index until it goes cold, and then the full scan arrives thousands of times at once.
    • Replicas for a write problem. Every replica applies every write; more replicas add WAL shipping load and nothing for a write-bound primary.
    • Scaling up a contention problem. More cores don’t help transactions queueing on one counter row; fix the hot row (contention patterns) or batch updates.
    • Forgetting the connections. Scaling the app tier from 20 to 200 pods multiplies connections tenfold; pool first.
    • Climbing too late. A split or shard migration needs spare capacity to copy data while serving traffic. Notion’s sharding retrospective lists “shard earlier” first; the same holds for every structural rung.

    Every rung is permanent operating cost

    A cache, a replica fleet, a CDC pipeline or a shard map is another thing to back up, monitor, upgrade and get paged about at 3 am. Count that cost, not just the capacity each rung adds.

    Cheat sheet

    Topic Remember
    Order Measure → tune → scale up → pool → cache → replicas → partition → split by domain → CQRS stores → shard → distributed SQL
    Reads vs writes Copies (cache, replicas) scale reads; only splitting data scales writes
    Biggest single node (RDS, Oct 2026) 192 vCPU and 1,536 GiB, or 128 vCPU and 4,096 GiB; 64 TiB storage; up to 256,000 IOPS
    Aurora Up to 15 replicas on shared storage, lag usually under 100 ms; 128 TiB volume, 256 TiB on recent versions
    RDS replicas Up to 15 per primary; asynchronous, so plan for read-your-writes
    Write ceiling sketch IOPS ÷ I/Os per write: 64,000 ÷ 8 ≈ 8,000 inserts/s with uncached random-key indexes
    Headroom Act well before saturation; at 3× yearly growth, 30% CPU becomes 90% in a year
    CQRS System of record plus CDC-fed search, warehouse and caches, seconds behind
    Shard when One domain’s writes or data exceed the biggest primary, within about a year

    How to say it in the interview

    “I’d start with one PostgreSQL primary, because it will carry us further than people expect, and climb only when metrics say so. First I’d find what saturates at peak. The cheap rungs come first: fix the heavy queries and indexes, scale up the instance, put PgBouncer in front, and cache hot reads with protection against stampedes. Reads beyond that go to replicas, with read-your-writes paths pinned to the primary. Writes are the real limit, so I’d partition the big time-ordered tables, split independent domains into their own databases, and feed search and analytics from CDC instead of querying the primary. If one domain still outgrows the biggest instance, I’d shard that domain by its natural key, or use distributed SQL if the team doesn’t want to own sharding.”

    Why not shard from day one? Sharding taxes every query and operation for years, and most products never need it. Shard the one domain whose writes outgrow a single machine, when projections show that within about a year.

    The primary is at 90% CPU and most of it is reads. What now? Check the top queries first; one bad plan can be most of it. Then cache hot reads and move lag-tolerant reads to replicas, keeping read-your-writes on the primary. A bigger instance buys time meanwhile.

    The primary is saturated by writes. Do replicas help? No. Every replica replays every write. Reduce the writes (fewer indexes, batching, removing redundant updates), partition so indexes stay small and deletes become drops, scale up for time, then split by domain or shard.

    When would you choose distributed SQL over sharding PostgreSQL yourself? When you need horizontal writes or multi-region strong consistency without building routing, resharding and cross-shard transactions yourself. You accept consensus latency, compatibility gaps and cost; for a large existing PostgreSQL estate, Citus may be the smaller migration.

    How do you know which rung comes next? Measure at peak: CPU by query shape, cache hit ratio, IOPS against limits, lock waits, connections and lag. The saturated resource picks the rung.

    Where to go next

    References and further reading

    1. Scaling PostgreSQL to power 800 million ChatGPT users (OpenAI, 2026)openai.comOne primary and nearly 50 read replicas serving millions of QPS; offloading writes, PgBouncer, cache leases, workload isolation and WAL fan-out limits.
    2. The growing pains of database architecture (Figma, 2023)figma.comFigma’s climb: a bigger RDS instance, read replicas, PgBouncer and new databases, then vertical partitioning by groups of tables.
    3. Amazon RDS: Hardware specifications for DB instance classesdocs.aws.amazon.comvCPU and memory of the largest classes (db.r8g.48xlarge, db.r7i.48xlarge, db.x2iedn.32xlarge), checked against the engine-support tables.
    4. Amazon RDS: DB instance storagedocs.aws.amazon.com64 TiB maximum storage for MySQL and PostgreSQL, gp3 up to 64,000 IOPS and io2 Block Express up to 256,000 IOPS.
    5. Amazon RDS: Quotas and constraintsdocs.aws.amazon.comUp to 15 read replicas per primary DB instance.
    6. Amazon Aurora: Replication with Auroradocs.aws.amazon.comUp to 15 Aurora Replicas on a shared cluster volume, with replica lag usually well under 100 ms.
    7. Amazon Aurora: Quotas and size limitsdocs.aws.amazon.comCluster volume limit of 256 TiB on recent Aurora PostgreSQL and MySQL versions, 128 TiB on earlier ones.
    8. Aurora PostgreSQL Limitless Database architecturedocs.aws.amazon.comShard groups of routers and shards, hash-sharded, collocated and reference tables behind one endpoint.
    9. Google Cloud Spanner overviewcloud.google.comAutomatic splits, synchronous Paxos replication, TrueTime-ordered transactions, up to 99.999% availability, GoogleSQL and PostgreSQL dialects.
    10. CockroachDB architecture overviewcockroachlabs.comData split into ranges replicated three ways by default with Raft, automatic range splits, and SERIALIZABLE and READ COMMITTED isolation.