The silent performance killers you never see coming.
Eloquent observers and events are powerful — they keep your code clean and decoupled. But they come at a cost. Every model operation (save, update, delete, create) triggers a cascade of events and observers. For bulk operations, this can be catastrophic. A 1,000-row update becomes 1,000 individual updates, each with its own event chain.
| Operation | Without Observers | With Observers | Overhead |
|---|---|---|---|
| Single User::create() | 2ms | 10-50ms | 5-25x slower |
| Bulk update (1,000 rows) | 5ms (1 query) | 5,000ms (1,000 queries + observers) | 1,000x slower |
| Bulk insert (1,000 rows) | 10ms (batch insert) | 10,000ms (1,000 inserts + observers) | 1,000x slower |
| Bulk delete (10,000 rows) | 15ms (one query) | 15,000ms + (N queries) | 1,000x slower |
Observers are great for single-record operations but catastrophic for bulk operations. A bulk update that should take 5ms takes 5 seconds — all because each record triggers its own observer chain.
Always wrap bulk operations in withoutEvents() to disable observers and events.
// This triggers UserObserver::updating()
// for EVERY single row!
User::where('role', 'guest')
->update(['role' => 'member']);
// Each row: SQL query + observer chain
// 1,000 rows = 1,000 queries + 1,000 observer chains
// Time: 5+ seconds
// No observers triggered!
User::withoutEvents(function () {
User::where('role', 'guest')
->update(['role' => 'member']);
});
// One SQL query. Zero observer overhead.
// Time: 5ms
// For single record, observers are fine.
// But if you want to skip them for specific cases:
$user = User::find(1);
$user->withoutEvents(function () use ($user) {
$user->update(['name' => 'New Name']);
// No events fired
});
// Or use the instance method
$user->updateQuietly(['name' => 'New Name']);
// Laravel 9+ has updateQuietly() specifically for this
// Insert 10,000 users
foreach ($newUsers as $userData) {
User::create($userData);
// Each create triggers:
// - saving event
// - creating event
// - created event
// - saved event
// All observers run 10,000 times!
}
// Time: 30+ seconds
// Batch insert without events
User::withoutEvents(function () use ($newUsers) {
User::insert($newUsers);
// One SQL INSERT with 10,000 rows
// Zero observers
// Zero events
});
// Time: 0.5 seconds
// Or use insertOrIgnore for duplicates
User::withoutEvents(function () use ($newUsers) {
User::insertOrIgnore($newUsers);
});
Bulk insert with withoutEvents() is 100x faster than individual creates with observers. 30 seconds → 0.3 seconds.
Even worse than updates: deleting with SoftDeletes trait triggers events on EVERY row.
// Each delete triggers:
// - deleting event
// - deleted event
// - SoftDeletes updates deleted_at
User::where('inactive', true)
->get()
->each->delete();
// Each row: 1 query + observer chain
// 1,000 rows = 1,000 queries
// Time: 5+ seconds
User::withoutEvents(function () {
User::where('inactive', true)
->delete(); // One DELETE query
// No observer chains
});
// Time: 5ms
// app/Observers/UserObserver.php
class UserObserver
{
public function created(User $user): void
{
// DON'T do this (synchronous)
// Mail::to($user)->send(new WelcomeMail($user));
// DO this (asynchronous)
SendWelcomeMailJob::dispatch($user->id);
UpdateStatsJob::dispatch($user->id);
SyncCrmJob::dispatch($user->id);
}
}
// app/Jobs/SendWelcomeMailJob.php
class SendWelcomeMailJob implements ShouldQueue
{
public function __construct(public int $userId) {}
public function handle(): void
{
$user = User::find($this->userId);
Mail::to($user)->send(new WelcomeMail($user));
}
}
User::withoutEvents(function () {
User::create([...]);
});
$user = new User();
$user->unsetEventDispatcher(); // Disable events for this instance only
$user->save(); // No events fired
// app/Providers/EventServiceProvider.php
protected $observers = [
User::class => [
UserObserver::class,
],
];
// UserObserver.php
public function shouldHandle(User $user): bool
{
// Skip observers for bulk operations
return !$user->withoutEvents ?? false;
}
withoutEvents() wraps a closure and disables all events.
updateQuietly() is an instance method that skips events for that specific update (Laravel 9+).
$user->updateQuietly(['name' => 'New Name']); // No events
$user->deleteQuietly(); // No events (Laravel 9+)
| Situation | Recommendation |
|---|---|
| Single record operations | Observers are fine (but move heavy work to queues) |
| Bulk updates (multiple records) | Use withoutEvents() to disable observers
|
| Batch insert (many new records) | Use insert() with withoutEvents()
| Batch delete | Use delete() on query builder with withoutEvents()
|
| Email sending in observers | ❌ DON'T — use queued jobs instead |
| External API calls in observers | ❌ DON'T — use queued jobs instead |
| Logging in observers | ✅ OK if minimal, but batch logs |
| Operation Type | Without withoutEvents() | With withoutEvents() | Gain |
|---|---|---|---|
| Bulk update (1,000 rows) | 5+ seconds | 5ms | 1,000x faster |
| Batch insert (1,000 rows) | 30+ seconds | 300ms | 100x faster |
| Batch delete (10,000 rows) | 15+ seconds | 15ms | 1,000x faster |
Topic 25: Logging I/O Trap — Every Log::info() opens a file. With 10,000 logs per request, your I/O dies. How to batch logs and save disk I/O.