On this page
Every Laravel model quietly fills in created_at and updated_at, and most of the time that is exactly what you want. The trouble starts when a counter bumps updated_at on every page view, a bulk insert leaves the columns empty, or a legacy table uses different column names. This article covers how Eloquent timestamps work, how to change them, and the places they silently do not run. Everything below was run against the real Laravel 13.35.0 Eloquent classes and MariaDB.
The default behavior
The migration helper $table->timestamps() adds both columns. Eloquent then sets them for you:
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->unsignedInteger('reads')->default(0);
$table->timestamps();
});
$post = Post::create(['title' => 'Hello']);
// created_at = 10:00:00, updated_at = 10:00:00Two details that surprise people, both confirmed in my test with a frozen clock:
$post->save()with nothing changed does not touchupdated_at. No changes means no UPDATE query at all.- Changing a field and saving moves only
updated_at(10:00 became 10:05).created_atnever changes.
Turn timestamps off
For tables like logs or pivots that have no such columns, tell Eloquent to stop. The current documentation uses PHP attributes:
use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
use Illuminate\Database\Eloquent\Model;
#[WithoutTimestamps]
class LogEntry extends Model
{
// ...
}The same thing is possible with #[Table(timestamps: false)]. I checked that both attributes make usesTimestamps() return false, and the insert ran without trying to write the missing columns. The older public $timestamps = false; property is still read by the framework source, so existing code keeps working, but the attribute form is what the docs show now.
Custom column names
Working with an old database that calls them creation_date and updated_date? Define two constants:
class Flight extends Model
{
public const CREATED_AT = 'creation_date';
public const UPDATED_AT = 'updated_date';
}Both columns were filled correctly in my run.
Store them as Unix timestamps
By default dates are stored in your database's standard date format. To store a different format, set the date format on the model. The docs show 'U', which is a Unix timestamp:
use Illuminate\Database\Eloquent\Attributes\DateFormat;
#[DateFormat('U')]
class Flight extends Model
{
// ...
}#[Table(dateFormat: 'U')] does the same. With an integer column the row stored 1767261600, and reading $flight->created_at still returned a Carbon object. Remember that the column must be able to hold that format, so I used an unsigned integer column for this test.
Update a record without touching updated_at
This is the one most people need. Think of a view counter: you do not want an article to look "recently edited" just because someone read it. The docs give this exact tool:
use Illuminate\Database\Eloquent\Model;
Model::withoutTimestamps(fn () => $post->increment('reads'));Results from my test, with the clock moved forward before each step:
| Operation | Did updated_at change? |
|---|---|
$post->save(), nothing changed | No |
$post->save(), field changed | Yes |
$post->touch() | Yes |
$post->increment('reads') | Yes |
withoutTimestamps(fn () => $post->increment('reads')) | No |
withoutTimestamps around a save() with changes | No |
Post::where(...)->update([...]) | Yes |
saveQuietly() | Yes |
Note the last row. saveQuietly() only mutes model events. It does not skip timestamps, which is a common mix-up. If you need both, wrap it in withoutTimestamps too. Also notice that increment() bumps updated_at by default, so a plain view counter will keep refreshing it unless you wrap it.
Where timestamps do not happen
Eloquent sets timestamps when it saves a model object or runs an Eloquent query builder update. Anything that skips the model skips them too. In my test:
DB::table('posts')->where(...)->update([...])changed the title but left updated_at alone.DB::table('posts')->insert([...])produced a row withcreated_atandupdated_atbothNULL.Post::insert([...])(bulk insert) did the same.insert()never creates model instances, so nobody sets the dates.
If you bulk insert, add the dates yourself with now() on each row, otherwise reports that filter on created_at will silently miss them. This is the same trade-off as in the earlier query optimization article: bulk operations are fast because they skip the model layer, and the model layer is also what fills in your timestamps.
Quick checklist
- No
created_at/updated_atcolumns? Use#[WithoutTimestamps]. - Different column names?
CREATED_ATandUPDATED_ATconstants. - Counter or background sync should not look like an edit?
Model::withoutTimestamps(). - Using
insert()orDB::table()? Set the dates yourself. - Want to bump
updated_atonly, for cache busting?$model->touch().
References
Free resources for this topic
Code snippets and fixes for this topic
Get one practical tip every week
New tutorials, fixes and free cheat sheets from DEV Talk, straight to your inbox. No spam, unsubscribe in one click.
Free download: Laravel interview questions (PDF)
Have a question about this post?
Ask the community: other developers and the author can answer.
Get notified about new articles
One click, no email needed. Turn it off anytime with the bell at the top.
Get new articles on your iPhone
Add DEV Talk to your Home Screen, open it, then tap the bell.
Enjoying the content?
If this article helped you, you can support the author with a coffee. It keeps new posts coming.
Comments 0
No comments yet
Be the first to share your thoughts.