Muting Eloquent Events with withoutEvents() and saveQuietly()
You've wired up an observer that sends a notification every time an Order is updated. Great, until someone runs a one-off script to backfill 40,000 historical orders and your notification queue — and your customers' inboxes — get absolutely buried. Eloquent gives you a clean way to opt specific operations out of the event system without ripping the observer out.
The problem in miniature
class OrderObserver
{
public function updated(Order $order): void
{
Notification::send($order->customer, new OrderUpdatedNotification($order));
}
}
This is exactly the behavior you want for real customer-facing updates. It is not what you want when a migration script touches every row to backfill a new column.
saveQuietly(): one model, no events
foreach ($ordersNeedingBackfill as $order) {
$order->shipping_zone = ZoneResolver::resolve($order->postal_code);
$order->saveQuietly();
}
saveQuietly() persists the model exactly like save(), but suppresses every Eloquent event that would normally fire — saving, updating, updated, saved, the works. Observers listening for those events simply don't run for this call. There's also deleteQuietly(), restoreQuietly(), and forceDeleteQuietly() for the same suppression on other lifecycle actions.
withoutEvents(): a whole block, no events
When you're touching many models across a block of code, wrapping each call in saveQuietly() gets repetitive. Model::withoutEvents() mutes events for everything inside its closure:
use App\Models\Order;
Order::withoutEvents(function () {
Order::whereNull('shipping_zone')
->cursor()
->each(function (Order $order) {
$order->shipping_zone = ZoneResolver::resolve($order->postal_code);
$order->save();
});
});
Everything inside that closure — including save() calls that would normally fire the full event chain — runs silently. Once the closure returns, event dispatching goes back to normal for the rest of the request or command.
A seeder example
Seeders are the other classic case. If your UserObserver sends a welcome email on created, you really don't want that firing 500 times while seeding a local dev database:
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
class UserSeeder extends Seeder
{
public function run(): void
{
User::withoutEvents(function () {
User::factory()->count(500)->create();
});
}
}
The line you shouldn't cross
Muting events is a tool for "this is bulk, administrative, or one-off data manipulation," not a substitute for fixing an observer that's too aggressive. If your normal application flow regularly needs to bypass its own event listeners to behave correctly, that's usually a sign the observer is doing something it shouldn't — like sending a notification on every minor field update instead of only the ones that matter. Reach for withoutEvents() and saveQuietly() for scripts, imports, and seeders; keep your real request-response cycle honest with the events firing as designed.