Skip to content

BlogEngineering Resources

Read Replicas Do Not Scale Writes

Read Replicas Do Not Scale Writes

"Add a read replica" can be a reasonable answer to a database scaling question in a system design interview. In a standard single-leader setup, replicas primarily add read-serving capacity and copies that may be promoted after a leader failure. They can also offload backups, analytical queries, or other work. Every write still enters through one leader, and each replica in the setup considered here replays that leader's write stream. Replicas do not partition or directly widen the leader's write path. They may indirectly improve write headroom when offload frees shared CPU, I/O, or another binding leader resource. Replica reads also require an explicit visibility contract.

Those boundaries lead to two useful follow-ups: what limits write throughput, and what may a replica read return?

Start with what is actually growing

"The database is struggling" leaves the bottleneck unspecified. At least four dimensions may be growing, with different responses:

  • Read volume. More queries against the same data. Candidates include indexes, caching, and read replicas.
  • Write volume. More inserts and updates per second. The leader's ability to accept, log, and commit transactions may become binding. Replicas do not directly expand that path, though read offload can help when reads consume the same constrained resources.
  • Data size. The working set outgrows memory, or the dataset outgrows one node's storage. Partitioning, archiving, or tiering may address this. A full replica copies the same dataset.
  • Geographic reach. Users far from the database see high latency. Regional replicas can serve local reads, while writes in a single-leader setup still travel to the leader.

A design should identify the growing dimension and binding resource before adding infrastructure. In an interview, one useful opening is: "Before I add anything, I'd want to know whether reads, writes, data size, or latency is the problem."

Why replicas do not directly add write capacity

In single-leader replication, all writes go to one node. The leader validates the transaction, writes it to its log (WAL in PostgreSQL, the binary log in MySQL), commits, and streams the log to its followers. Each follower applies the changes to its own copy of the data.

A replica can serve reads from its local copy, but keeping that copy current requires processing the leader's write stream. With ten replicas, each change is processed on eleven nodes: the leader and ten followers. That work maintains redundant copies rather than dividing the incoming write stream among leaders. The leader also spends resources serving replication streams, although the amount depends on the database and configuration.

When the leader's write path is binding, useful options include batching, schema or index changes, and removing unnecessary writes. Sharding can distribute writes when the partition key spreads load, transactions stay mostly within partitions, hot shards are controlled, and cross-shard coordination does not become the new bottleneck. Read or operational offload is a smaller intervention when shared leader CPU or I/O is the actual constraint.

A replica is not a freshness guarantee

Replication in two widely used open-source databases is asynchronous unless configured otherwise. The PostgreSQL documentation states that streaming replication is asynchronous by default, and the MySQL documentation says the same of MySQL replication. Under asynchronous replication, a commit can become visible on the leader before a replica has applied it. The lag varies with configuration, workload, long-running transactions, and network conditions, so the design needs an explicit tolerance rather than a universal estimate.

Lag can produce two anomalies worth naming:

  • Read-your-writes violations. A user updates a profile, but the next page load reaches a replica that has not applied the change.
  • Non-monotonic reads. Consecutive reads reach replicas at different positions, and the second returns older data than the first.

Routing a user's reads to the leader after a write avoids replica lag for those reads, subject to transaction isolation and snapshot rules, but returns traffic to the leader. Pinning a session to one healthy, monotonically advancing replica can provide monotonic reads while that routing remains stable and each read takes a new snapshot; rerouting or replica failure requires the application to re-establish the guarantee. Tracking a committed log position, such as an LSN in PostgreSQL or GTID in MySQL, and waiting for a replica to apply it guarantees visibility through that position. It does not establish that the read includes later commits, and waiting adds latency and application plumbing.

Synchronous acknowledgement also does not make an arbitrary replica read current. A commit acknowledged by one standby says nothing about a different replica selected for the read. The acknowledgement stage matters as well: receipt, durable flush, and application provide different guarantees. PostgreSQL can wait for visibility on an acknowledging standby with synchronous_commit = remote_apply, but the read must still reach an appropriate standby and use a new enough snapshot. Write acknowledgement and read routing must be designed together for the required visibility.

Synchronous and asynchronous replication allocate risk differently

With asynchronous replication, the leader confirms a commit without waiting for replica confirmation. If the leader then fails and a follower missing that transaction is promoted, an acknowledged write can be lost. The PostgreSQL documentation warns that asynchronous replication creates this data-loss exposure, with the amount determined by what had reached the promoted standby.

With synchronous replication, the leader waits for the configured acknowledgement before confirming the commit. Write latency then includes that wait, and the durability guarantee depends on what acknowledgement stage and how many standbys or quorum members are required. Commits stall only while the requirement cannot be satisfied and no timeout, fallback, reconfiguration, or degradation policy has taken effect. Such policies can preserve availability by weakening the original durability guarantee, so the design should state when that change is allowed.

MySQL's semisynchronous mode waits for at least one replica to receive the change rather than apply it. This can reduce the loss window, but the failover result still depends on which replica is promoted and what data it has. A design discussion should name the tolerated loss exposure, latency budget, acknowledgement rule, and behavior when the rule cannot be met.

One cluster, two read policies

Read policy can vary by subsystem within one cluster. Consider a marketplace whose payment tables and activity feed share a PostgreSQL cluster.

Assume payment status has low read volume and requires read-your-writes behavior. After a completed charge, the next status read could go to the leader or to a replica that has applied the charge's LSN. The first policy consumes leader capacity; the second may wait for replication. Transaction isolation remains a separate part of the contract.

For the activity feed, assume the product accepts several seconds of staleness in exchange for greater read capacity. Replica reads fit that budget. Stable session routing can reduce backward movement while the selected replica remains healthy, but the application needs a policy for rerouting.

Failover behavior depends on operational readiness. Payment reads may pause during failure detection, promotion, and routing changes. Feed reads may continue from surviving replicas only if routing permits it and those replicas remain ready; they may also expose additional lag. Any acknowledged writes absent from the promoted standby remain subject to the chosen replication guarantee. The standby configuration, connection pools, caches, promotion path, and post-promotion capacity should be tested under representative load before these outcomes are treated as dependable.

The alternative: replicas for availability only

Replicas may exist purely for failover while all application reads go to the leader. This can fit when the leader handles the combined read and write load and the team wants one read path without application-level replica-lag handling. Leader routing avoids replica lag for new reads, but transaction isolation and snapshot behavior still determine what each transaction can observe.

The leader must be sized for both workloads, so this design gives up replica read capacity. It still carries replication and standby-operating costs. A passive standby must keep data and configuration current, remain promotable, and have enough capacity after promotion. Cold caches or pools, routing errors, replication lag, and configuration drift can extend the interruption or change the data-loss exposure. Representative failover tests are needed to validate detection, promotion, routing, recovery time, and post-promotion performance.

Check cheaper controls when read capacity is the problem

When replicas are proposed to relieve read load, inspect query and connection behavior first. A missing index can turn point lookups into scans. A cache can remove hot repeated reads from the database. Connection pooling can reduce connection churn and constrain concurrency, although it does not make expensive queries cheaper.

These controls change different parts of the system and can be combined: an index reduces per-query work, a cache removes repeated reads, a pool manages connection concurrency, a replica adds read-serving compute, and a shard partitions data and, under suitable workload conditions, the write stream. Availability and durability requirements remain independent reasons to deploy replicas even when query tuning can solve the immediate capacity problem.

A precise design answer names what is growing, identifies the binding resource, defines the visibility contract for each subsystem, and states the replication behavior during failure. A read replica is appropriate for read-serving capacity only when that capacity is constrained and the application can enforce or tolerate the resulting read semantics.

Share this post