Prevent Stale Reads in Laravel with Read Replicas | Mohamed Said       [Skip to content](#main)  [ ![](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png) Mohamed SaidLaravel Backend Engineer ](https://www.msaied.com) - [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [About](https://www.msaied.com#about)

           [  Contact](https://www.msaied.com#contact) Menu 

Menu
----

Close 

 - [HomeStart here](https://www.msaied.com)
- [ProjectsCase studies](https://www.msaied.com/projects)
- [ArticlesEngineering notes](https://www.msaied.com/articles)
- [CertificatesCredentials](https://www.msaied.com/certificates)
- [AboutHow I work](https://www.msaied.com#about)
- [ContactGet in touch](https://www.msaied.com#contact)

  [Start a conversation](https://www.msaied.com#contact) [WhatsApp](https://wa.me/201094619204) [Email](mailto:hello@msaied.com) 

 1. [Home](https://www.msaied.com)
2. /
3. [Articles](https://www.msaied.com/articles)
4. /
5. [Laravel](https://www.msaied.com/articles?category=laravel)
6. /
7. Laravel Read Replicas: Prevent Stale Reads After a Write

   [Laravel](https://www.msaied.com/articles?category=laravel) 

 Laravel Read Replicas: Prevent Stale Reads After a Write
=========================================================

 Laravel 13.38 introduces PreventStaleDatabaseReads middleware and usingWritePdo() to stop users from seeing outdated data after a write when read replicas lag behind the primary server.

 ![](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp) [Mohamed Said](https://www.msaied.com#person) Published 22 Oct 2026 · Updated 26 Oct 2026 · 4 min read

ShareCopy linkCopied

 ![Laravel Read Replicas: Prevent Stale Reads After a Write](https://cdn.msaied.com/806/7655717eef251177fe662e2802c93d39.png) 

  On this page +1. [The Problem: Stale Data After a Write](#the-problem-stale-data-after-a-write)
2. [PreventStaleDatabaseReads Middleware](#preventstaledatabasereads-middleware)
3. [How the Middleware Works](#how-the-middleware-works)
4. [usingWritePdo() for Targeted Code Blocks](#usingwritepdo-for-targeted-code-blocks)
5. [Testing the Behavior](#testing-the-behavior)
6. [Key Takeaways](#key-takeaways)

 The Problem: Stale Data After a Write
-------------------------------------

When a Laravel application routes writes to a primary database server and reads to one or more replicas, replication lag can cause a user to see outdated data immediately after saving a change. A user updates a post title, gets redirected to the show page, and the replica hasn't caught up yet — so the old title appears.

Laravel's built-in `sticky` option in `config/database.php` partially addresses this by routing reads to the write server for the remainder of the same request. But a `PUT` that redirects to a `GET` is two separate requests. The redirect starts fresh, reads from the replica again, and the stale data problem returns.

PreventStaleDatabaseReads Middleware
------------------------------------

Laravel 13.38 ships a `PreventStaleDatabaseReads` middleware that bridges the gap across the redirect boundary. Add it to the `web` group in `bootstrap/app.php`:

```php
use Illuminate\Foundation\Http\Middleware\PreventStaleDatabaseReads;

->withMiddleware(function (Middleware $middleware) {
    $middleware->web(append: [
        PreventStaleDatabaseReads::class,
    ]);
})

```

Or scope it to specific routes:

```php
Route::middleware(['auth', PreventStaleDatabaseReads::class])->group(function () {
    Route::get('/posts/{post}', [PostController::class, 'show']);
    Route::put('/posts/{post}', [PostController::class, 'update']);
});

```

The default window is two seconds. Override it with the `for()` method:

```php
Route::middleware(PreventStaleDatabaseReads::for(15))->group(function () {
    // ...
});

```

Set the window to match the longest replication lag you observe in your own metrics — a window shorter than the lag still lets stale data through.

### How the Middleware Works

On each request the middleware runs three steps:

1. **Before the controller** — reads a `_database_modified_at` timestamp from the session. If it falls inside the configured window, it calls `useWriteConnectionWhenReading()` so every `SELECT` in the request hits the write server.
2. **The request runs** as normal.
3. **After the response** — calls `hasModifiedRecords()`. If any rows were inserted, updated, or deleted, it writes the current timestamp back to the session for the next request.

usingWritePdo() for Targeted Code Blocks
----------------------------------------

The existing `onWriteConnection()` method forces a single query onto the write server, but relationships and subsequent queries built from that result still use a replica:

```php
// Only the first query uses the write server
$user = User::onWriteConnection()->where('email', $email)->first();

// This query may still hit a lagging replica
$drafts = $user?->posts()->whereNull('published_at')->get();

```

The new `DB::usingWritePdo()` method forces every read inside a callback to the write server:

```php
use Illuminate\Support\Facades\DB;

$drafts = DB::usingWritePdo(function () use ($email) {
    $user = User::query()->where('email', $email)->first();
    return $user?->posts()->whereNull('published_at')->get();
});

```

The method returns whatever the callback returns. When the callback finishes (or throws), the connection reverts to its previous state. Queries already inside a `DB::transaction()` use the write server automatically, so wrapping them in `usingWritePdo()` is unnecessary.

Testing the Behavior
--------------------

Each entry in the query log includes a `readWriteType` key (`read` or `write`). Use it to assert that post-write reads hit the correct server:

```php
public function test_reads_after_an_update_use_the_write_connection(): void
{
    $post = Post::factory()->for(User::factory()->create())->create();

    DB::forgetRecordModificationState();

    $this->actingAs($post->user)
        ->put("/posts/{$post->id}", ['title' => 'New title'])
        ->assertRedirect();

    DB::forgetRecordModificationState();
    DB::enableQueryLog();

    $this->actingAs($post->user)->get("/posts/{$post->id}")->assertOk();

    $this->assertSame('write', DB::getQueryLog()[0]['readWriteType']);
}

```

> **Note:** Use `DatabaseMigrations` or `DatabaseTruncation` instead of `RefreshDatabase`. The `RefreshDatabase` trait wraps tests in a transaction, which forces all queries to the write server regardless of middleware, making the assertion meaningless.

Key Takeaways
-------------

- The `sticky` option only covers reads within the same request; it does not survive a redirect.
- `PreventStaleDatabaseReads` uses the session to carry write awareness across requests.
- Configure the window with `PreventStaleDatabaseReads::for($seconds)` — match it to your replica lag.
- `DB::usingWritePdo()` forces all reads inside a callback to the write server, including relationship queries.
- Queries inside `DB::transaction()` already use the write server; no extra wrapping needed.
- Use `readWriteType` in the query log to write reliable tests for this behavior.

---

*Source: [Laravel Read Replicas: Prevent Stale Reads After a Write — Laravel News](https://laravel-news.com/laravel-prevent-stale-database-reads)*

- [Laravel](https://www.msaied.com/articles?search=Laravel)
- [Database](https://www.msaied.com/articles?search=Database)
- [Read Replicas](https://www.msaied.com/articles?search=Read%20Replicas)
- [Middleware](https://www.msaied.com/articles?search=Middleware)
- [Laravel 13](https://www.msaied.com/articles?search=Laravel%2013)

 Frequently asked questions 
---------------------------

  Why doesn't the sticky option prevent stale reads after a redirect?The sticky option routes reads to the write server only for the duration of the current request. A form submission that issues a PUT and then redirects to a GET is two separate requests. The GET starts a fresh connection and reads from a replica again, which may not have replicated the change yet.

   How do I configure the duration of the PreventStaleDatabaseReads window?Pass the number of seconds to the static for() method: PreventStaleDatabaseReads::for(15). The default is two seconds. Set it to at least as long as the maximum replication lag you observe in your replica metrics.

   When should I use usingWritePdo() instead of onWriteConnection()?Use usingWritePdo() when you need multiple queries — including relationship queries — to all hit the write server. onWriteConnection() only applies to the single query it is chained on; any subsequent queries built from that result (such as loading a relationship) will still use a replica.

   ![Mohamed Said](https://cdn.msaied.com/01M22N44A70A5MC2S599JP0MPH.webp)About the author
----------------

[Mohamed Said](https://www.msaied.com#person)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

[About](https://www.msaied.com#about) [GitHub ↗](https://github.com/EG-Mohamed) [LinkedIn ↗](https://www.linkedin.com/in/msaiedm/) [WhatsApp ↗](https://wa.me/201094619204) [Email Address ↗](mailto:hello@msaied.com) [My CV ↗](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)  

   [Previous articleSkuul: Open-Source School Management Built on Laravel 13 and Livewire 4](https://www.msaied.com/articles/skuul-open-source-school-management-built-on-laravel-13-and-livewire-4) [Next articleDecide with Jev Episode 3: Turn AI Answers into Decisions with PHP Rules](https://www.msaied.com/articles/decide-with-jev-episode-3-turn-ai-answers-into-decisions-with-php-rules)  

   On this page
-------------

1. [The Problem: Stale Data After a Write](#the-problem-stale-data-after-a-write)
2. [PreventStaleDatabaseReads Middleware](#preventstaledatabasereads-middleware)
3. [How the Middleware Works](#how-the-middleware-works)
4. [usingWritePdo() for Targeted Code Blocks](#usingwritepdo-for-targeted-code-blocks)
5. [Testing the Behavior](#testing-the-behavior)
6. [Key Takeaways](#key-takeaways)

 ###  Have a technical challenge?

 Tell me what you’re building. I reply within two working days.

[Start a conversation](https://www.msaied.com#contact) 

   Related articles
-----------------

 [ ![](https://cdn.msaied.com/805/a4dfa0e7c7e3c01fb99f0f970866675b.png) AI · 3 min read### Decide with Jev Episode 3: Turn AI Answers into Decisions with PHP Rules

23 Oct 2026 ](https://www.msaied.com/articles/decide-with-jev-episode-3-turn-ai-answers-into-decisions-with-php-rules) [ ![](https://cdn.msaied.com/804/adc0feaa0650d379192acac1e92a111e.png) Laravel · 4 min read### Skuul: Open-Source School Management Built on Laravel 13 and Livewire 4

21 Oct 2026 ](https://www.msaied.com/articles/skuul-open-source-school-management-built-on-laravel-13-and-livewire-4) [ ![](https://cdn.msaied.com/802/4b381a7cf7b496975aa9775d2454494f.png) Laravel · 4 min read### Screenshot: Capture Images and PDFs from Blade Views and URLs in Laravel

21 Oct 2026 ](https://www.msaied.com/articles/screenshot-capture-images-and-pdfs-from-blade-views-and-urls-in-laravel) 

  Have a technical challenge?
----------------------------

Tell me what you’re building. I reply within two working days.

 [Discuss your project ↗](https://www.msaied.com#contact) 

  © 2026 Mohamed Said · Built with Laravel, meant to last.Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

 - [Home](https://www.msaied.com)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [GitHub](https://github.com/EG-Mohamed)
- [LinkedIn](https://www.linkedin.com/in/msaiedm/)
- [WhatsApp](https://wa.me/201094619204)
- [Email Address](mailto:hello@msaied.com)
- [My CV](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
- [Sitemap](https://www.msaied.com/sitemap.xml)
