Most Laravel developers call ->save() a hundred times a day.
Almost none of them know what happens after that line executes.
I was one of them — until I inherited a codebase where Eloquent was behaving in ways I could not explain. Models not updating. Events not firing. Timestamps wrong.
I had to open the Laravel source code and actually read it.
What I found surprised me.
The Entry Point — It Is Not Just "Save to Database"
When you call $model->save(), most developers assume Laravel just fires an SQL query.
It does not.
The first thing save() does is make a decision.
It checks one property on your model:
$this->exists
This single boolean decides everything. If $exists is false, Laravel runs an INSERT. If it is true, Laravel runs an UPDATE.
That sounds simple. But it has real consequences that catch developers off guard.
How Laravel Knows INSERT vs UPDATE
The $exists property is set to true the moment a model comes from the database — whether you use find(), first(), get(), or any query that hydrates a model from existing records.
It is false when you create a new model instance manually:
$user = new User(); // $exists = false → will INSERT
$user = User::find(1); // $exists = true → will UPDATE
Where this causes real problems:
When you clone a model.
$original = User::find(1);
$copy = clone $original;
$copy->email = 'new@example.com';
$copy->save(); // Updates the ORIGINAL record — not a new one
The cloned model still has $exists = true and the original primary key. Laravel will update the original row, not create a new one.
The fix — reset it manually:
$copy = clone $original;
$copy->exists = false;
$copy->id = null;
$copy->save(); // Now it INSERTs correctly
Knowing $exists exists saves you from this bug entirely.
Before Anything Touches the Database
Before Laravel runs any SQL, it fires model events.
This is the order:
-
saving— fires on every save, insert or update -
creating— fires only on INSERT -
updating— fires only on UPDATE
If any listener on these events returns false, the save is cancelled completely. No query runs.
This is how validation inside Observers works — and also why sometimes your save() silently does nothing. An Observer somewhere returned false and you never knew.
Always check your Observers when a save mysteriously fails.
After events, Laravel does something most developers never think about:
Dirty checking.
Laravel does not update every column. It only updates columns that actually changed since the model was loaded. This is tracked through $dirty — an array of changed attributes.
$user = User::find(1);
$user->name = 'Haseeb'; // marks name as dirty
$user->save(); // only UPDATEs name — not every column
This is a performance feature built into Eloquent. But it also means:
If you set a value to the same thing it already was — nothing updates. No query runs. This is called a no-op save and it is intentional.
The Actual Query
Once events pass and dirty attributes are collected, Laravel builds the SQL.
For an INSERT it uses performInsert(). For an UPDATE it uses performUpdate().
Both go through setKeysForSaveQuery() — a method that determines which column to use as the identifier for the WHERE clause on updates.
By default this is your primary key. But you can override it:
protected function setKeysForSaveQuery($query)
{
$query->where('custom_column', $this->custom_column);
return $query;
}
This is useful when you have composite keys or non-standard primary keys — something default Eloquent does not handle well out of the box.
Timestamps are handled here too.
If your model uses timestamps — which is the default — updated_at is set automatically on every UPDATE. created_at and updated_at are both set on INSERT.
You do not touch them. Laravel handles them before the query runs.
After the Save
Once the query executes, Laravel fires the after-events:
-
saved— fires on every save -
created— fires after INSERT -
updated— fires after UPDATE
One thing most developers never use — $wasRecentlyCreated:
$user->save();
if ($user->wasRecentlyCreated) {
// This was a new record — send welcome email
}
This flag is set to true only on INSERT in the current request. It resets on the next load from database.
Useful when you want to do something only on first creation — without adding extra logic to figure out whether you just inserted or updated.
Real World Implications
1. Never call save() inside a loop
// Bad — fires one query per iteration
foreach ($users as $user) {
$user->status = 'active';
$user->save();
}
// Good — one query for everything
User::whereIn('id', $userIds)->update(['status' => 'active']);
Every save() inside a loop is a separate database round trip. On 100 records that is 100 queries. Use bulk update() instead.
2. Use save() when you need events. Use update() when you need speed.
save() fires all model events and goes through Eloquent's full lifecycle.
update() called directly on a query builder skips all of that — no events, no observers, no dirty checking.
Both are correct. Choose based on what you actually need.
3. Debugging weird Eloquent behaviour
When save() does nothing — check:
- Is an Observer returning
false? - Is the attribute actually dirty or same as before?
- Is
$existsset correctly?
These three questions explain 90% of mysterious Eloquent save issues.
Why This Matters Beyond Trivia
Understanding what happens inside ->save() is not about memorising Laravel internals.
It is about understanding the decisions your framework makes on your behalf — so you can work with them instead of against them.
The best Laravel developers I have worked with have all done this at least once. Not because they had to. Because they wanted to understand what they were actually building on.
You do not need to do this for every method.
But doing it for the ones you call every single day — like save() — changes how you debug, how you architect, and how you write code that does not surprise you later.
What Is Your ->save() Equivalent?
Every framework has methods we call without thinking.
What is one Laravel method you use daily but have never fully looked into?
Drop it in the comments — I will pick one and write about it next.
Haseeb Mirza is a Laravel backend engineer building SaaS platforms, AI-powered applications, and automation systems. Open to senior backend roles and consulting projects.
Top comments (0)