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
- [13.x] Allow float values in Unique
whererule by @sertxudev in #62059 - [13.x] Remove dead
untilhigher order collection proxy by @xurshudyan in #62057 - [13.x] Fix ofMany() alias inside closures by @xurshudyan in #62051
- [13.x] Fix existence and aggregate queries on self-referencing one-of-many relations by @xurshudyan in #62043
- [13.x] Support CountCrashesAsExceptions on queued listeners, mailables and notifications by @Taldres in #62048
- [13.x] Make health route compatible with route caching across machines by @percymamedy in #62049
- [13.x] Restore the namespaced Cloud logging formatter by @xurshudyan in #62068
- [13.x] Run the Memcached cache lock and tagged cache integration tests by @xurshudyan in #62069
- [13.x] Fix
mode()returning truncated values for floats by @xurshudyan in #62063 - [13.x] Reset
NotificationFailedflag before each channel send by @xurshudyan in #62074 - [13.x] Respect child
#[Connection]attribute over inherited$connectionproperty by @xurshudyan in #62065 - [13.x] Fix
whereBetweenandwhereNotBetweenwith collection bounds by @xurshudyan in #62062 - [13.x] Fix altering SQLite tables with a foreign key to a table name that needs quoting by @sumaiazaman in #62084
- [13.x] Fix clone() on relations by @xurshudyan in #62081
- [13.x] Release the unique lock when dispatching a scheduled job fails by @sumaiazaman in #62083
- [13.x] Fix incorrect docblocks by @xurshudyan in #62097
- [13.x] Fix PHPStan error when passing a closure to PendingBatch::allowFailures() by @xurshudyan in #62096
- [13.x] Remove unused imports by @xurshudyan in #62087
- [13.x] Free the previous chunk in
lazy()andlazyById()by @xurshudyan in #62086 - [13.x] Add PreventStaleDatabaseReads middleware by @jackbayliss in #62078
- [13.x] Dispatch UniqueJobSkipped when a unique broadcast event is skipped by @jackbayliss in #62077
- [13.x] Remove dead test files by @xurshudyan in #62101
- [13.x] Fix TypeError in date comparison rules on non-date values by @GrahamCampbell in #62103
- [13.x] Fix incorrect docblocks by @xurshudyan in #62100
- [13.x] add usingWritePdo to Connection by @jackbayliss in #62099