What Is refreshForUpdate()?
Laravel has long provided two separate tools for working with database rows inside transactions:
refresh()— reloads a model's attributes from the database.lockForUpdate()— appendsFOR UPDATEto a query, preventing other transactions from reading or modifying the row until the lock is released.
What was missing was a way to do both on an existing model instance without rebuilding the query manually. Laravel 13.27 fills that gap with refreshForUpdate(), contributed by @stevebauman in #61247.
How It Works Under the Hood
The implementation is concise:
public function refreshForUpdate()
{
if (! $this->exists) {
return $this;
}
return $this->refreshUsingQuery(
$this->newQueryWithoutScopes()->lockForUpdate()
);
}
It delegates to refreshUsingQuery() — the same internal helper that powers refresh() — but passes a query that already has lockForUpdate() applied. That helper:
- Scopes the query to the model's primary key.
- Routes the query through
useWritePdo()so a read replica cannot serve a lock you are about to depend on. - Calls
firstOrFail()to fetch the row. - Replaces the model's raw attributes in place.
- Reloads any relations that were already eager-loaded.
- Syncs the original attribute state.
The only addition refreshForUpdate() makes over a plain refresh() is the FOR UPDATE lock.
Before and After
Before Laravel 13.27
To safely decrement stock while preventing concurrent overselling, you had to re-query the model explicitly inside the transaction:
public function purchase(Product $product): Response
{
DB::transaction(function () use ($product) {
$product = Product::query()
->lockForUpdate()
->findOrFail($product->getKey());
if ($product->stock === 0) {
throw new RuntimeException('The product is out of stock.');
}
$product->decrement('stock');
});
// ...
}
This works, but it requires you to reassign $product and remember to call lockForUpdate() every time.
After Laravel 13.27
With refreshForUpdate() you call the method directly on the model instance:
public function purchase(Product $product): Response
{
DB::transaction(function () use ($product) {
$product->refreshForUpdate();
if ($product->stock === 0) {
throw new RuntimeException('The product is out of stock.');
}
$product->decrement('stock');
});
// ...
}
The model is refreshed with the latest database values and locked for the duration of the transaction — in one readable line.
Key Takeaways
refreshForUpdate()is available from Laravel 13.27 onward.- It combines
refresh()andlockForUpdate()into a single Eloquent method call. - The query is always routed to the write PDO connection, preventing stale reads from a replica.
- If the model does not exist (
$this->exists === false), the method returns early without querying. - It must be called inside a
DB::transaction()block; aFOR UPDATElock has no effect outside a transaction. - Already-loaded Eloquent relations are refreshed automatically alongside the model's own attributes.
Source: Pessimistic Locking in Laravel Eloquent with refreshForUpdate() — Laravel News