When database reads become slow or expensive, two additions often appear on the architecture diagram: a cache and a read replica. Both can reduce pressure on the primary database. They do it in different ways, and choosing the wrong one can add another system to operate while leaving the bottleneck in place.
A cache avoids repeating work for data that can be reused. A read replica provides another database instance that can execute read queries. Before adding either, identify whether the problem is repeated retrieval of the same results, insufficient capacity for varied queries, an inefficient query, or contention with writes. Those problems do not have the same remedy.
Start with the measured read pattern. Use a cache when many requests can reuse the same answer within an acceptable freshness window. Use a read replica when you need more capacity for database queries that cannot usefully be cached.
- First establish what the primary database cannot handle
- A cache removes repeat work; a replica adds query capacity
- Make data freshness a property of the read
- Design the failure path before the fast path
- Count the entire cost of the extra layer
- Choose a path that matches the observed workload
- Continue exploring data-intensive architecture
First establish what the primary database cannot handle
High read volume alone does not prove that another data layer is needed. A few missing indexes, an expensive join, excessive requests from the application, or an undersized database may account for most of the load. Fixing those issues can be cheaper and easier to operate than introducing replication or cache invalidation.
Separate three observations: how much time each query takes, how often the same data is requested, and how much of the primary’s capacity reads consume. An inefficient query remains inefficient on a replica. A cache helps little if nearly every request asks for a different result. Conversely, repeatedly calculating the same public product page from the database wastes work even if the individual query is reasonably fast.
Establish a baseline with query latency, query frequency, database CPU and I/O pressure, connection usage, and application response time. Look at peak periods as well as averages. The useful question is not “Are reads slow?” but “Which reads are creating the constraint, and how reusable are their results?”
Cloud.Rich’s guide to scaling without overengineering recommends addressing the observed bottleneck before adding distributed components. Database read scaling is where that principle becomes particularly concrete: adding capacity and avoiding repeated work are separate interventions.
A cache removes repeat work; a replica adds query capacity
With a typical cache-aside design, the application checks the cache for a result. On a miss, it queries the database and stores the result for later requests. The benefit depends on repeated access to the same keys or results. The application must decide what to cache, how long an entry remains valid, and what to do when data changes or the cache is unavailable. AWS’s caching pattern documentation describes both the miss penalty and the possibility of stale entries in this model.
A read replica receives changes from a primary and serves read-only queries against a database copy. It can run a broader range of queries without the application defining a cache key and invalidation rule for every result. It does, however, require the application to route suitable reads away from the primary and tolerate the replica’s consistency model. In common asynchronous replication designs, a recently committed change may not yet be visible on the replica; PostgreSQL’s replication documentation explicitly notes this possibility.
| Decision factor | Cache | Read replica |
|---|---|---|
| Best fit | Repeated requests for reusable data or computed results | Many varied read queries competing for database capacity |
| Data shape | Selected keys, objects, or responses | A queryable database copy, subject to the replication design |
| Freshness problem | Expiration and invalidation of individual entries | Delay before primary changes become visible |
| Main failure consequence | Misses can send a sudden surge of requests to the database | Reads may fail, lag, or return to an already busy primary |
| Application change | Choose cacheable reads and define fallback and invalidation behavior | Classify reads and route them according to consistency needs |
Neither option is a general substitute for the other. A replica can execute an uncommon reporting query that has no useful cache hit rate. A cache can serve a popular result without asking any database instance to execute its query again. Both may be valuable when those two read patterns coexist.
Make data freshness a property of the read
“Eventually consistent is acceptable” is too broad a rule for an entire application. A product description can often tolerate a short delay. The balance shown immediately after a payment, a newly changed permission, or the result of a just-submitted action may need a fresher path. Route reads according to what the user or operation must observe.
A cache needs an explicit validity policy. A time-to-live limits how long an entry can remain, but it does not make an entry current before it expires. Invalidation after a write can improve freshness, yet it becomes harder when one write affects several cached views, aggregates, or search results. Cache keys must also account for tenant, user, locale, and authorization context where those change the answer. An incomplete key can turn a performance feature into an incorrect response.
A replica has a different freshness boundary. Even though it contains the database schema and supports queries, asynchronous replication can lag behind the primary. A read immediately following a write may need to stay on the primary, or the application may need a way to wait for an appropriate replication position. Routing every read to a replica without classifying these cases can make a successful write appear to disappear.
Define the freshness requirement per operation before choosing a data path. “Read-only” does not mean “safe to serve from an older copy.”
Cloud.Rich’s analysis of stateful architecture asks where state should live and who owns its guarantees. Caches and replicas extend that question: once an application has multiple places from which to read, it must decide which one is authoritative for each operation.
Design the failure path before the fast path
A cache normally sits in front of the authoritative store. If it fails, the application may be able to query the database directly. That is only a safe fallback if the database can absorb the resulting misses. A cache restart, mass expiration, or popular key expiring under peak traffic can concentrate requests on the primary at the worst moment. Expiration spread, request coalescing, bounded retries, and capacity for a defined fallback load may be more important than another increment in cache hit rate. AWS’s cache validity guidance describes how simultaneous expiration can increase database pressure.
If a read replica falls behind, the application needs a policy: continue serving older data, send selected reads to the primary, limit the affected feature, or stop serving reads that cannot meet their freshness requirement. Blindly redirecting all replica traffic to the primary may reproduce the overload the replica was introduced to solve. A replica used for reads should also not be assumed to provide automatic failover; read scaling and recovery are related but distinct designs.
Operational visibility should follow these failure paths. For a cache, monitor hit and miss rates by important request class, eviction, memory pressure, fallback traffic, and database load during misses. For a replica, monitor replication delay, query latency, cancellations or errors, and how much read traffic returns to the primary. A healthy aggregate hit rate or an available replica endpoint can conceal a failing critical read path.
Count the entire cost of the extra layer
A cache consumes memory and network capacity and adds application work for keys, invalidation, fallback, and testing. It can still be economical when a small, frequently requested set of results accounts for substantial database work. Its value should be measured in avoided database load and improved application latency, not merely the number of cached entries.
A read replica adds database compute, storage, replication traffic, monitoring, and routing logic. It may make sense when the application needs a sizeable amount of sustained read capacity for varied queries. If those queries require large scans or poor plans, the replica can become another expensive database with the same performance problem.
The options also change future work. Cache invalidation must evolve when data dependencies change. Replica routing must evolve when a feature’s freshness requirement changes. Both require failure testing and an owner for the new operating surface. Compare those costs with simpler measures such as query improvements, connection management, or resizing the existing database before approving a new layer.
Estimate the load each option will actually remove from the primary, then include the cost of freshness rules, failure handling, monitoring, and application changes.
Choose a path that matches the observed workload
If a small set of reads dominates traffic and the results have a defensible freshness window, start with a narrowly scoped cache. Keep the database authoritative, choose a simple validity rule, and test what happens when every request misses. Expand caching only when the measured benefit survives the operational cost.
If reads are diverse, database-compatible, and constrained mainly by aggregate query capacity, evaluate a read replica. First identify which operations can accept replication delay. Keep freshness-sensitive reads on an appropriate path, and define what happens when the replica lags or fails. Do not use the replica to hide inefficient queries.
If both patterns are material, use both deliberately: cache the genuinely reusable results and route suitable uncached queries to a replica. Avoid making every request pass through both layers by default. Each additional route creates another consistency and failure case to explain, observe, and test.
The decision can be revisited as the workload changes. A cache that works for a popular catalog may contribute little to a growing set of personalized queries. A replica that handles diverse reads may still waste capacity repeatedly computing the same expensive result. Measure the read mix after each change rather than treating the first scaling addition as the final architecture.
Fix wasteful queries first. Avoid repeat work with a cache when results are reusable; add a replica when useful queries need more read capacity. Add both only when the workload clearly contains both problems.
Continue exploring data-intensive architecture
Read performance leads to broader decisions about state placement, infrastructure ownership, and the cost of sustained capacity.
Cloud strategy, architecture and infrastructure decisions explained without vendor noise.
