Scaling Reads
Most systems are read-heavy, so this is where most scaling happens. The good news: reads are the easy
direction — you can make copies of data freely.
Prerequisites: Caching, Replication
Time to read: ~16 minutes
Why reads are the easy direction
🚨 Reads scale more easily than writes because you can duplicate data freely. A read doesn’t change
anything, so you can serve it from a cache, a replica, a CDN, or a precomputed copy — as many copies as
you want, all consistent-enough. Writes must be coordinated (only one source of truth); reads don’t.
📐 And most systems are read-heavy — often 100:1 or more. So scaling reads is where the leverage is,
and the read/write ratio should be the first
thing you compute, because it tells you reads are the problem to solve.
🚨 A rough ladder — apply the cheaper, higher-leverage ones first:
1. Cache — the biggest lever
🚨 The first and most impactful move. Serve repeated reads from memory instead of the database.
Because reads repeat (the 80/20 rule — a small set of data serves most reads), a cache with a good hit
rate removes most of the database’s read load.
95% cache hit rate → the database sees only 5% of read traffic → 20× reduction
Layers, cheapest/closest first (caching):
- Browser / HTTP cache — free, no request at all.
- CDN — serve static and cacheable content near users.
- Application / distributed cache (Redis) — hot query results, computed data.
- Database buffer pool — automatic (size your RAM).
🚨 Hit rate is everything — above ~90% a cache is a huge win, below ~50% barely worth it. Ask “what
hit rate do we expect?” first. And handle stampedes.
2. Read replicas — scale beyond one machine
Add read-only copies of the database; route reads to them,
writes to the primary. Scales read throughput linearly with replicas.
✅ Scales reads horizontally, offloads the primary.
❌ 🚨 Replication lag — replicas are slightly stale, causing the
read-your-writes bug (user posts, reads a replica, doesn’t
see it). Handle by routing a user’s reads to the primary briefly after their write.
🎙️ “For read scaling I’d cache first — a 95% hit rate cuts database read load 20×. Then read replicas
for what misses the cache, routing writes to the primary and reads to replicas — with the caveat of
replication lag, so I’d read a user’s own recent writes from the primary.”
3. Denormalization — avoid the joins
Precompute and store data in the shape reads need, so a read is one lookup instead of a multi-table
join. → NoSQL modeling, Relational modeling
✅ Fast reads (no join cost).
❌ Writes must update multiple copies; risk of inconsistency. Trade write complexity for read speed.
4. Precomputation / materialized views — do the work at write time
🚨 Move expensive work from read time (frequent) to write time (rare). Instead of computing an
aggregate on every read, compute it once when the data changes and store the result. A
materialized view or a precomputed feed.
📐 This is the fan-out-on-write idea: for a news feed read 100× per write,
precompute each user’s feed at write time so reads are pure lookups. → CQRS
5. Search index / read-optimized store — CQRS
For complex read patterns the primary database serves poorly (search, analytics), maintain a separate
read-optimized store (Elasticsearch, a read model), fed by
CDC or events. → CQRS
6. Do less work / return less data
- Field selection / pagination — don’t return more than needed.
→ Pagination
- Compression — less to transfer.
- Better indexes — → Indexing.
Read consistency: the trade-off you’re making
🚨 Every read-scaling technique trades freshness for speed/scale (caches are stale, replicas lag,
denormalized copies can drift, precomputed data is a snapshot). This is fine for most data but must be a
deliberate choice per data type:
- Tolerates staleness (view counts, catalogues, feeds) → cache/replicate/precompute aggressively.
- Needs freshness (account balance at checkout, inventory) → read from the primary / a consistent
store.
→ Consistency Models
🎙️ “These are all trading freshness for read scale, so I’d apply them per data type — cache and
replicate the catalogue and feeds hard, but read inventory and balances from the primary where a stale
read costs money.”
Where reads eventually hit a wall
Read scaling is easy until:
- Hot keys — one key so popular that even replicas/caches for it saturate one node. Needs special
handling. → Hot Keys
- Write volume outgrows the primary — replicas all replicate from the primary, so if writes
saturate the primary, replication itself suffers. Then you need to scale writes (sharding).
→ Scaling Writes
- The working set outgrows RAM — even cached, if the hot set doesn’t fit, hit rate collapses.
⚖️ Trade-offs
| Technique |
Gain |
Cost |
| Cache |
10-100× read offload |
Staleness; invalidation; failure mode |
| Read replicas |
Horizontal read scaling |
Replication lag; read-your-writes bug |
| Denormalization |
Fast reads, no joins |
Write complexity; drift risk |
| Precompute / materialized view |
Reads are lookups |
Write-time cost; snapshot staleness |
| CQRS / search index |
Reads served by the right store |
Sync pipeline; eventual consistency |
| Return less data |
Less transfer/compute |
— |
In the real world
- The cache-then-replica pattern is near-universal — almost every read-heavy system uses caching as
the first line (absorbing most reads) and read replicas as the second, precisely because reads
dominate and this pair is cheap and effective.
- Fan-out-on-write for feeds (Twitter’s timeline) is the canonical precompute-at-write example —
reads are so much more frequent than writes that precomputing each user’s feed at write time makes
reads pure cache lookups, which is essential at their scale.
→ News Feed
- The read-your-writes bug is one of the most common real-world consistency issues from read
replicas — “I posted a comment and it disappeared” — and its fix (read own recent writes from the
primary) is standard.
🚨 Interview traps
- Not computing the read/write ratio — it tells you reads are the problem.
- Jumping to replicas before caching — cache is the bigger, cheaper lever.
- Ignoring replication lag / read-your-writes when adding replicas.
- Applying staleness-tolerant techniques to data that needs freshness (balances, inventory).
- Not handling hot keys — where read scaling breaks.
- Forgetting cache hit rate determines whether caching helps at all.
🎙️ Soundbites
- “Reads are the easy direction — you can make copies freely. Most systems are 100:1 read-heavy, so
read scaling is where the leverage is. I’d compute the ratio first.”
- “Cache first — a 95% hit rate cuts database read load 20×. Then read replicas for the misses, with
the caveat of replication lag: I’d route a user’s own recent writes to the primary so they don’t
think their post vanished.”
- “For expensive reads I’d precompute at write time — a materialized view or fan-out-on-write — moving
the work from frequent reads to rare writes. That’s the CQRS idea.”
- “Every one of these trades freshness for scale, so I’d apply them per data type: cache and replicate
the catalogue and feeds hard, but read inventory and balances from the primary where staleness costs
money.”
- “Read scaling holds until a hot key saturates one node, or writes outgrow the primary that replicas
copy from — then it becomes a write-scaling problem.”
🛠️ Try it
1. Measure cache impact. Take a read-heavy endpoint hitting a database. Add a cache and measure
database QPS and endpoint latency before and after at various hit rates. Watch a 95% hit rate cut
database load ~20× — the biggest read-scaling lever, quantified.
2. Add a read replica and hit the lag bug. Route reads to a replica, writes to the primary. Post
something and immediately read it from the replica — watch it not be there (replication lag). Then
route own-recent-writes to the primary and watch it work.
3. Precompute vs compute-on-read. Build a feed two ways: compute it on each read (fan-out-on-read),
and precompute it at write time (fan-out-on-write). Compare read latency and the read/write cost
balance. See how precomputing trades cheap frequent reads for more expensive rare writes.
4. Denormalize a join. Take a query with a five-table join, denormalize the data into one
read-optimized table, and compare read speed. Then update the underlying data and feel the write-side
complexity of keeping the denormalized copy correct.
Check yourself
1. Why are reads easier to scale than writes?
Because you can duplicate read data freely, while writes must be coordinated. A read doesn't change
anything, so the same data can be served from many independent copies — caches, read replicas, CDNs,
precomputed materializations — all serving simultaneously without conflicting, and you can add as many
copies as you need for more read throughput. Writes are fundamentally different: there must be a single
source of truth for each piece of data (or you get conflicts), so writes must be routed to and
coordinated at that source, which doesn't parallelize the same way — scaling writes past one machine
requires sharding, which sacrifices joins and transactions. This asymmetry, combined with the fact that
most systems are heavily read-dominated (often 100:1), is why read scaling is where most of the
practical work happens and why it's the "easy direction": caching and replication are cheap, effective,
and don't require the painful trade-offs that write scaling does. It's also why computing the read/write
ratio first is important — it usually confirms reads are the problem, pointing you at the easy tools.
2. Why is caching preferred over read replicas as the first read-scaling move?
Because it's a bigger, cheaper lever. Caching serves reads from memory (nanoseconds to sub-millisecond)
instead of the database, and because reads repeat heavily (the 80/20 rule — a small hot set serves most
reads), a good hit rate removes most of the database's read load entirely: a 95% hit rate means the
database sees only 5% of read traffic, a 20× reduction, from a single Redis instance or even in-process
cache. Read replicas, by contrast, still involve full database queries (just on other machines), cost
more (running additional database instances), and add replication lag and its consistency bugs. So
caching gives a larger reduction in load for less infrastructure and often less complexity — you'd
typically reach for it first, absorbing the bulk of reads, and add replicas as the *second* line for
what still misses the cache and needs a real database query. The caveat is that caching's value depends
entirely on the hit rate: above ~90% it's transformative, below ~50% it barely helps, so you ask "what
hit rate do we expect?" before relying on it, and you handle cache stampedes and the cache's own
failure mode (what happens when it's down and the database gets 100% of traffic).
3. What is the read-your-writes problem with read replicas, and how do you handle it?
Read replicas lag behind the primary — writes go to the primary and take some time (milliseconds to
seconds, more under load) to replicate. So if a user makes a write (posts a comment) and then
immediately reads — and that read is routed to a replica that hasn't yet received the write — they
don't see their own action, appearing as though it failed ("I posted a comment and it disappeared").
They may then retry and create a duplicate. It's one of the most common real-world consistency bugs
from read replicas. The standard fixes: route a user's reads to the *primary* for a short window after
they make a write (a session flag or timestamp, e.g. "read from primary for 5 seconds after this user's
last write"); or track the write's replication log position and only serve that user from a replica
that has caught up past it; or read user-modifiable data (their own posts, profile) from the primary
while reading everything else from replicas. The general principle is read-your-writes consistency —
users must see their own writes immediately even if others see them slightly later — and it's cheap to
provide by being selective about which reads go to the primary.
4. What does "precompute at write time" mean and when is it worth it?
It means doing expensive work once, when data changes (a write), and storing the result, so that reads
become cheap lookups instead of expensive computations. Instead of computing an aggregate, assembling a
feed, or running a complex query on every read, you compute it when the underlying data is written and
save the answer — via a materialized view, a denormalized table, or fan-out-on-write. It's worth it
when reads are much more frequent than writes, which is the common case: for a news feed read 100 times
per write, computing the feed on every read (fan-out-on-read) means 100 expensive assemblies, while
precomputing each follower's feed at write time means the write does the work once and the 100 reads
are pure lookups — a huge net saving. The trade-off is that writes become more expensive (they now do
the precomputation and may update many copies) and the precomputed data is a snapshot that can be
briefly stale, but when the read/write ratio is high, trading cheap-frequent-reads for
expensive-rare-writes is a large win. This is the essence of CQRS (separate read and write models) and
the reason fan-out-on-write powers timelines at scale. It stops being worth it when writes are frequent
relative to reads, or when the precomputation fans out to too many copies (a celebrity with 100M
followers — hence hybrid approaches).
5. Why does every read-scaling technique involve a freshness trade-off, and how do you manage it?
Because every technique works by serving reads from something *other* than the single authoritative
source at the moment of the read: a cache holds a copy that may be stale, a read replica lags the
primary, a denormalized table holds a copy that can drift from the source, a precomputed view is a
snapshot from when it was last computed, and a search index is eventually consistent with the database.
All of them gain speed and scale precisely by not going to the always-current source of truth, which
means they can return data that's slightly out of date. You manage it by making the freshness decision
*per data type*, deliberately, based on the cost of a stale read. Data that tolerates staleness — view
counts, follower counts, product catalogues, recommendations, feeds — can be cached, replicated,
denormalized, and precomputed aggressively, because a few seconds of staleness harms nothing. Data that
needs freshness — an account balance at the moment of a transaction, inventory at checkout, a seat's
availability during booking — must be read from the primary or a strongly-consistent store, because a
stale read causes real harm (overselling, overdrawing, double-booking). So a well-designed read-scaling
strategy isn't uniform: it applies the staleness-tolerant techniques hard to the data that can take it,
while routing the correctness-critical reads to the authoritative source, paying for freshness only
where it matters.
Further reading