They remove read pressure, but they also move stale data into places users notice fast.

Your app gets slow, somebody points at the primary database, and the next sentence usually arrives with far too much confidence: “Let’s add a read replica.”
The query chart improves. The write path stays on the primary. Everybody feels responsible and modern for about two days.
Then a user updates a profile, refreshes the page, and sees the old value. An order status flips to paid, but the dashboard still says pending. Support looks at the admin panel and gets a different answer again because one request hit the writer and the next one hit the replica. Nothing is technically down. The system is simply telling the truth at different speeds.
That is the part teams miss. A read replica is not just a scaling move. It is a consistency decision with user-facing consequences, and if you do not make that trade-off explicitly, your application will make it for you in the least convenient places.
The seductive part is real
Read replicas are useful for a reason.
Amazon RDS describes them plainly: a read replica is a read-only copy of a DB instance, and you can reduce load on the primary by routing read queries to it. That is real value. For read-heavy workloads, replicas can buy headroom without forcing an immediate redesign of your schema, query patterns, or caching layer.
The pitch sounds clean:
- Keep writes on the primary.
- Move heavy reads to one or more replicas.
- Reduce load on the writer.
- Call it scale.
The first three parts are fine. The fourth part is where people get sloppy.
If your application cannot tolerate stale reads for a given workflow, then that workflow did not become scalable when you added a replica. It became split-brain at the application layer. Softer failure, same pain.
The replica is behind by design
This is not a bug in your cloud provider. It is the model.
AWS says RDS updates read replicas with the database engine’s asynchronous replication method. MySQL says the source commit and the replica commits are independent and asynchronous. That is the core mechanic. The primary can commit and return success before the replica has applied the same change.
That single fact changes how you should talk about replicas.
A replica is not “another copy that serves reads.” It is “another copy that catches up.” Most of the time the lag is small enough that nobody notices. Sometimes it is not. Under load, during long transactions, after bursts, or when the replica is undersized relative to the writer, the distance between “committed” and “visible on the replica” becomes product behavior.
AWS also recommends matching compute and storage resources between source and read replica for PostgreSQL so replication can operate effectively. That recommendation exists because replicas do not magically stay close under pressure. They have to keep up.
If they do not keep up, you have not created more truth. You have created a second answer.
The failure mode is not abstract. It lands in ordinary user flows.
Replica inconsistency does not usually announce itself as some grand distributed-systems catastrophe. It sneaks in through normal screens.
Think about the patterns that break first:
- A user writes, then immediately reads the same record back.
- An admin panel reads operational state right after a background job updates it.
- A checkout flow writes order state, then renders a confirmation page from a read connection.
- A webhook handler updates a row and the next request hits a replica before the change arrives there.
Those are not edge cases. That is Tuesday.
The ugly part is that teams often misdiagnose these bugs. They call them cache bugs, frontend race conditions, queue timing issues, or flaky tests. Sometimes they are. Sometimes the database just answered from two different moments in time and the app had no policy for deciding which one was allowed to speak.
Laravel already hints at the problem
Laravel’s read and write connection support makes this easy to set up, which is good. It also makes it easy to lie to yourself, which is less good.
If you configure separate read and write hosts, Laravel can route selects to the read connection and writes to the write connection. Then it offers one small escape hatch: the sticky option.
Laravel’s docs say that if sticky is enabled and a write has happened during the current request cycle, subsequent reads in that same request will use the write connection. That is not a convenience feature. It is Laravel quietly admitting that read-after-write consistency matters.
The configuration looks like this:
'mysql' => [
'read' => [
'host' => [
env('DB_READ_HOST', '127.0.0.1'),
],
],
'write' => [
'host' => [
env('DB_WRITE_HOST', '127.0.0.1'),
],
],
'sticky' => true,
'driver' => 'mysql',
'database' => env('DB_DATABASE', 'app'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
];That helps, but only within one request.
If request A writes data and request B arrives a second later on another worker, sticky does nothing for you. If a job writes status in the background and the browser polls another endpoint, sticky does nothing for you. If a different service reads from the replica, sticky does nothing for you.
So yes, turn it on. No, it does not solve the actual architectural question.
A faster primary and a stale replica are not the same success
This is the conceptual mistake that keeps repeating: teams optimize the database host and forget to define the data contract.
If you move reads to replicas, you need to answer three very specific questions:
- Which reads may be stale?
- For how long can they be stale before the product is wrong?
- Which user journeys must always hit the writer after a state change?
Without those answers, “use the replica for reads” is not architecture. It is hope with DNS.
Here is a small Laravel example that looks harmless:
public function store(Request $request): JsonResponse
{
$order = Order::create([
'user_id' => $request->user()->id,
'status' => 'paid',
]);
$freshOrder = Order::query()->findOrFail($order->id);
return response()->json([
'id' => $freshOrder->id,
'status' => $freshOrder->status,
]);
}In one request with sticky enabled, you are probably fine.
Now split that flow across endpoints:
- POST /orders writes the order
- frontend redirects to GET /orders/{id}
- that second request reads from a replica
Now you have a product decision whether you wanted one or not. The user either sees the new state immediately or they do not. A replica made that decision cheaper, not better.
Monitoring lag is necessary. It is not the whole answer.
AWS gives you a ReplicaLag metric. Use it.
It tells you how far behind a read replica is, in seconds, relative to the source instance. That is a useful operational signal. It can tell you the replica is struggling, a workload changed, or the underlying instance class is no longer appropriate.
But ReplicaLag is not a product guarantee.
A low average lag does not mean your critical read-after-write path is safe. A one-second lag is still a broken payment confirmation page if your user expects the page to reflect the write that just succeeded. A brief spike is still enough to make support distrust the dashboard. Monitoring tells you the database is drifting. It does not tell you whether the application is allowed to care.
That is why the design conversation has to happen above the metric.
There are good reasons to choose replicas anyway
This is not an anti-replica article. It is an anti-fantasy article.
Read replicas are a solid fit when the workload is genuinely tolerant of slightly older data:
- reporting dashboards
- analytics queries
- search indexes built from delayed data paths
- activity feeds where seconds of drift are acceptable
- internal tools where the operator understands “eventual” actually means something
They are a bad default for:
- post-write confirmation screens
- auth, billing, quota and permission decisions
- inventory and booking flows
- webhook-driven state transitions users can see immediately
- admin panels people rely on during incidents
That split matters more than the instance size, the number of replicas, or the cloud vendor.
Replicas do not replace the other scaling conversations
AWS makes another point that gets ignored in architecture meetings: RDS does not autoscale read replicas for you. You create them manually. That matters because teams often talk about replicas as if they were a general elasticity feature instead of a specific topology choice.
A replica also does not fix a bad query plan, a missing cache, or an overloaded writer doing too much non-read work. It can hide those problems for a while, which is different from solving them.
Promotion is a different conversation again, because a promoted replica can become a standalone writable instance, but that is failover behavior, not proof that your application model was safe for stale reads in the first place. People mix those ideas together constantly. They should not.
The better question is not “can we add a replica?”
The better question is: what problem are you refusing to solve directly?
Sometimes the real answer is query shape. You need better indexes, not another database host.
Sometimes it is caching. The data is read-heavy and slow-changing, so a cache or precomputed view is the honest fix.
Sometimes it is workload isolation. Reporting should not share the same path as transactional reads.
Sometimes it is that the product needs stronger consistency than you wanted to pay for, and the writer needs to stay in the hot path for those reads.
Replicas are useful when you have already answered those questions and still need more read capacity.
They are dangerous when you use them to postpone the conversation.
If you choose replicas, choose them with rules
This is the operating stance I would actually trust:
- Keep transactional and user-visible post-write reads on the writer.
- Turn on Laravel sticky so same-request reads do not betray you immediately.
- Route only explicitly stale-tolerant workloads to replicas.
- Monitor ReplicaLag, but do not confuse it with a correctness guarantee.
- Review whether the replica is masking a query-design problem every time lag becomes a talking point.
That is a much less glamorous story than “we scaled the database with replicas.”
It is also more honest.
The trade-off is the feature
Adding a read replica can reduce pressure on your primary. AWS says exactly that, and for the right workloads it is a smart move.
What it does not do is make consistency free.
If your team talks about read replicas only as a scaling plan, it is already one architectural layer too low. The real decision is which parts of your product are allowed to read yesterday’s truth, even if yesterday only lasted half a second.
That is the design. The replica is just the bill.


