A new ID at each privilege change defeats fixation (Session Fixation); Auth::login() calls regenerate(true) for you. Mind the argument: plain regenerate() keeps the old record, and the sessions table here still held the old row afterwards. regenerate(true) deletes it; invalidate() also empties the data, and with regenerateToken() it is logout.
PHP's file handler locks the session file (Session Storage Handlers), but Laravel 2,157 takes no lock: each request writes back the array it loaded, so the slower of two overlapping requests wins. block() serializes requests that share a session, using a cache lock (Atomic Locks):
$add = function (Request $request, string $sku) {
sleep($request->integer('wait')); // a slow payment-gateway call
$request->session()->push('cart.items', $sku);
};
Route::get('/add/{sku}', $add);
Route::get('/b/add/{sku}', $add)->block(lockSeconds: 10, waitSeconds: 10);
Route::get('/cart/items', fn () => session('cart.items'));for p in '' b/; do
rm -f jar; curl -s -c jar $B/cart > /dev/null
curl -s -b jar -o /dev/null -w "slow %{time_total}s\n" "$B/${p}add/SLOW?wait=2" &
sleep 0.3
curl -s -b jar -o /dev/null -w "fast %{time_total}s\n" $B/${p}add/FAST; wait
curl -s -b jar $B/cart/items; echo
donefast 0.022306s slow 2.028357s ["SLOW"] slow 2.049021s fast 1.795837s ["SLOW","FAST"]
Unblocked, FAST was saved and then overwritten. Blocked, it waited 1.8 s and both items survived; past waitSeconds it would throw LockTimeoutException. Block only routes that write.