Skip to content

[13.x] Add PreventStaleDatabaseReads middleware - #62078

Merged
taylorotwell merged 2 commits into
laravel:13.xfrom
jackbayliss:PreventStaleDatabaseReads
Oct 19, 2026
Merged

taylorotwell merged 2 commits into
laravel:13.xfrom
jackbayliss:PreventStaleDatabaseReads

Conversation

@jackbayliss

@jackbayliss jackbayliss commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

With using read/write connections and sticky connections I wanted to protect against users seeing stale data, as when using sticky the next request the user makes after a write goes back to the replica.

We have seen our replication time around 0-1 seconds average, but up to 15 (😱) depending on the operations occurring.

Today this can have a genuine effect:

  1. A user changed a title of a post
  2. They then manually refresh, and its the old title
  3. They create a bug report / get confused / re edit it etc.

This PR adds an opt in PreventStaleDatabaseReads middleware - so no b/c 🥇

When a user makes a write, it stores a timestamp in the session. For the next few seconds (2 by default), that user’s reads use the write connection, so they always see their own changes. After that, reads go back to the replica.

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

// or per route...
Route::middleware(PreventStaleDatabaseReads::class)->group(function () {
    Route::get('/posts/{post}', [PostController::class, 'show']);
    Route::put('/posts/{post}', [PostController::class, 'update']);
});

The delay can also be fully customized:

PreventStaleDatabaseReads::for(15)

We can handle this ourselves in userland, but felt like it would be something nice for the framework to do.. as it already offers sticky connections etc.

Couple things:

  • It only guarantees users see their own writes. Other users may still read from a replica.
  • It’s session based, so not ideal for API stuff imo
  • It only tracks the default connection, for now- as thats enough for us today.

I found rails has middleware like this also before sending it, so thought this could be of interest.

Feel free to adjust etc, as wasn't 100% on the naming or throw it away if theres no interest.

@github-actions

Copy link
Copy Markdown

Thanks for submitting a PR!

Note that draft PRs are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

$response = $next($request);

if ($connection->hasModifiedRecords()) {
$request->session()->put('_database_modified_at', Date::now()->getTimestamp());

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with underscores cos... feels more internal in my head

@jackbayliss
jackbayliss marked this pull request as ready for review October 19, 2026 13:42
@taylorotwell
taylorotwell merged commit 213b1e6 into laravel:13.x Oct 19, 2026
62 checks passed
@taylorotwell

Copy link
Copy Markdown
Member

Cool 👍

@trevorgehman

Copy link
Copy Markdown
Contributor

Just a question, but is this race condition relevant here? #30996

@jackbayliss
jackbayliss deleted the PreventStaleDatabaseReads branch October 20, 2026 15:03
@jackbayliss

Copy link
Copy Markdown
Contributor Author

@trevorgehman it could still apply I guess, though it means it'll use the read connection if it does overlap (not sure how rare that is). Possibly doing it via the cache is nicer, but I haven't run into the problem myself yet.

@trevorgehman

Copy link
Copy Markdown
Contributor

@trevorgehman it could still apply I guess, though it means it'll use the read connection if it does overlap (not sure how rare that is).

Actually what will happen is the session will be overwritten and _database_modified_at will be lost. So every request after that will use the read connection.

Possibly doing it via the cache is nicer, but I haven't run into the problem myself yet.

I think it is a rare situation, but it is easily reproducible. It tends to surface when you have long-running requests, such as pulling in a chart that takes a few seconds to load.

I actually don't think this is a serious issue, and using the session to store this sort of data is appropriate IMO. I want to just point this out in case it happens to someone else and they can't figure out what's going on :)

Thank you for your PR!

@patrickomeara

Copy link
Copy Markdown
Contributor

https://tim.macdonald.au/sticky-database-connections-across-requests-with-laravel/

Love that this is in the framework now

@jackbayliss

Copy link
Copy Markdown
Contributor Author

@patrickomeara Dope, I didn't realise Tim had a post about it 😅!

@patrickomeara

Copy link
Copy Markdown
Contributor

Great minds! @jackbayliss @timacdonald ;)

@timacdonald

Copy link
Copy Markdown
Member

Yooooo. Love this ❤️ Great work getting something into the FW, Jack.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants