Laravel read replicas: why users see old data after saving
If your Laravel app sends writes to one database server and reads to replicas, you can hit a bug that never shows up locally: a user edits a post title, saves, lands back on the page, and sees the old title. Nothing failed. The read went to a replica that had not copied the change yet. Laravel 13.38, released in October 2026, adds two tools for exactly this case.
Why sticky does not save you
A read/write connection in config/database.php has a sticky option. With it on, once a request writes, the reads that follow go to the write server. The catch is the scope: it lasts for the rest of that request only. A normal form is a PUT that redirects to a GET, and that is two requests. The redirect starts with a fresh connection, so the page you show after saving reads from a replica again.
The fix for users: PreventStaleDatabaseReads
The new middleware remembers the write across requests by putting a timestamp in the session. After a request writes to the default connection, that user's reads go to the write server for the next two seconds, then back to the replicas. Append it to the web group in bootstrap/app.php:
use Illuminate\Foundation\Http\Middleware\PreventStaleDatabaseReads;
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
PreventStaleDatabaseReads::class,
]);
})
Or put it on a few routes first, with a longer window if your replicas need it:
Route::middleware(PreventStaleDatabaseReads::for(15))->group(function () {
// routes where a user edits and then views
});
Pick the window from your own replica lag metrics, not from a guess. A window shorter than the real lag lets the old data through. Three limits are worth knowing before you rely on it: it needs a session, it only tracks the default connection, and it only covers the user who wrote. Another user loading the same page can still get the older copy.
The fix for code: DB::usingWritePdo()
Some code needs fresh data no matter who wrote it, such as a job that runs right after a webhook updates a row. onWriteConnection() already exists, but it applies only to the query it starts. Load a relationship from the result and that query goes to a replica. The new method sends every read inside the callback to the write server and restores the previous setting afterwards, even if the callback throws:
$drafts = DB::usingWritePdo(function () use ($email) {
$user = User::query()->where('email', $email)->first();
return $user?->posts()->whereNull('published_at')->get();
});
Code already inside DB::transaction() does not need it: queries in a transaction use the write server anyway.
Queue workers have the opposite problem
In a web request sticky ends with the response. A queue:work process keeps the same connection between jobs, so one job that writes can send every later job's reads to the write server until the worker restarts. Laravel 13.36 added an opt-in reset, Worker::$flushStickyConnections = true in a service provider's boot(). It is off by default, so check jobs that read right after another job writes before you turn it on. With the database queue driver, also give the jobs table its own connection through DB_QUEUE_CONNECTION, or reserving a job sets the flag again.
Testing it without fooling yourself
Each query log entry has a readWriteType key, read or write, so a test can save, request the page, and assert the first query was a write. Use DatabaseMigrations or DatabaseTruncation for that test. RefreshDatabase wraps each test in a transaction, every query in a transaction uses the write server, and the assertion would pass without the middleware.
usingWritePdo().Next step: if you run replicas, look up your worst replica lag, add PreventStaleDatabaseReads to the routes where users edit then view, set the window above that lag, and write the readWriteType test without RefreshDatabase. Both features are by Jack Bayliss, in #62078 and #62099. Source: Laravel News, Laravel Read Replicas: Prevent Stale Reads After a Write.
Notes from readers