News

Prevent Stale Database Reads in Laravel 13.38

Published
Prevent Stale Database Reads in Laravel 13.38 image

The Laravel team released v13.38.0 with a PreventStaleDatabaseReads middleware for applications that read from database replicas, a usingWritePdo() method that sends every read in a callback to the write connection, a health route that works with routes cached on a different machine, and more.

PreventStaleDatabaseReads Middleware

Jack Bayliss added an opt-in middleware for applications with separate read and write database connections. A replica can fall behind the write server, so a user who saves a change and reloads the page can see the old data.

After a request writes to the default database connection, the middleware stores a timestamp in the session. For the next two seconds, that user's reads go to the write connection. After that, reads go back to the replicas. You can append it to the web group:

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

Or apply it to a group of routes with a longer window:

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

The middleware needs a session, and it only tracks the default connection. It only covers the user who made the write, so other users can still read older data from a replica.

See #62078 for more details.

DB::usingWritePdo()

Jack Bayliss also added usingWritePdo() to the database connection. The existing onWriteConnection() method only applies to the query it starts, so a relationship loaded from the result still reads from a replica:

// The posts query still runs on a replica
$posts = User::onWriteConnection()
->where('email', $email)
->first()
?->posts()
->whereNull('published_at')
->get();

The new method sends every read inside the callback to the write connection, then restores the previous setting, even if the callback throws:

$posts = DB::usingWritePdo(fn () => User::query()
->where('email', $email)
->first()
?->posts()
->whereNull('published_at')
->get()
);

See #62099 for more details.

CountCrashesAsExceptions on Listeners, Mailables, and Notifications

Dennis Petersmann made the #[CountCrashesAsExceptions] attribute from Laravel 13.34 work on queued listeners, mailables, and notifications. Before this release, it only worked on jobs.

On the other three, a worker that crashed, for example from running out of memory, was not counted toward maxExceptions, so the listener or mail was retried with no limit when $tries was 0:

#[MaxExceptions(3), CountCrashesAsExceptions]
class ExportOrders implements ShouldQueue
{
public $tries = 0;
 
public function handle(OrdersImported $event)
{
// ...
}
}

See #62048 for more details.

Child Connection Attribute Overrides an Inherited Connection

Volodya Kurshudyan fixed models ignoring the #[Connection] attribute when a parent class sets the $connection property. Queries for the child model went to the parent's connection, with no error:

abstract class BaseModel extends Model
{
protected $connection = 'mysql';
}
 
#[Connection('reporting')]
class SalesReport extends BaseModel {}
 
(new SalesReport)->getConnectionName(); // Now returns "reporting"

See #62065 for more details.

Unique Job Fixes

Sumaia Aktar fixed unique jobs scheduled with Schedule::job() keeping their lock when the push to the queue fails. With the default uniqueFor of 0, the lock never expired, so the scheduler skipped the job on every later run, even after the queue came back. The scheduler now releases the lock and rethrows the exception. Laravel 13.37 made the same change for dispatch() and queued listeners. See #62083.

Jack Bayliss also made broadcast events that implement ShouldBeUnique dispatch the UniqueJobSkipped event when they are skipped, the same way queued jobs do. See #62077.

Lower Memory Use in lazy() and lazyById()

Volodya Kurshudyan changed lazy() and lazyById() to free the previous chunk before fetching the next one. Before this change, two chunks were held in memory at once, so peak memory was about twice what it needed to be. The chunk() and chunkById() methods already worked this way.

See #62086 for more details.

Release Notes

You can see the complete list of new features and updates below and the diff between 13.37.0 and 13.38.0 on GitHub. The following release notes are directly from the changelog:

v13.38.0

Paul Redmond photo

Staff writer at Laravel News. Full stack web developer and author.

Sponsored

acquaintsoft logo
Acquaint Softtech

Hire Laravel developers with AI expertise at $20/hr. Get started in 48 hours.

Visit Acquaint Softtech

The latest

View all →
PHP Security Releases Fix Two Critical RCE Vulnerabilities image

PHP Security Releases Fix Two Critical RCE Vulnerabilities

Read article
Critical PHP Security Release Coming Tomorrow (October 21) image

Critical PHP Security Release Coming Tomorrow (October 21)

Read article
Critical Token Guard Auth Bypass Fixed in Laravel 13.29 and 12.68 image

Critical Token Guard Auth Bypass Fixed in Laravel 13.29 and 12.68

Read article
Run Large PlanetScale Queries in Laravel With DB::olap() image

Run Large PlanetScale Queries in Laravel With DB::olap()

Read article
Fluent Rule Builders for All Laravel Validation Rules image

Fluent Rule Builders for All Laravel Validation Rules

Read article
Laravel URL AI Transformer: Extract Structured Data From URLs image

Laravel URL AI Transformer: Extract Structured Data From URLs

Read article