Every MongoDB developer hits this question sooner or later. You have a created_at field to store. You could use a Unix timestamp — just a number, portable, familiar. Or you could use MongoDB's native ISODate — built into the query language, aggregation pipeline, and drivers.
I've gone back and forth on this across multiple projects, and I've landed on a rule of thumb that works for most cases. But first, let's look at what each option actually gives you.
ISODate: The Native Way
MongoDB's native date type is ISODate — a 64-bit integer representing milliseconds since Unix epoch, wrapped in a BSON Date type.
// Inserting a date
db.events.insertOne({
event: 'user_signup',
created_at: new Date() // ISODate("2026-07-11T12:00:00Z")
});
When you query it back, MongoDB drivers in most languages map it to the native date type:
- Node.js →
Dateobject - Python →
datetimeobject - Java →
Dateobject - Go →
time.Timeobject
This means you can do date operations directly:
// Query documents from the last 7 days
db.events.find({
created_at: { $gte: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) }
});
// Aggregate by month
db.events.aggregate([
{ $group: {
_id: { $month: "$created_at" },
count: { $sum: 1 }
}}
]);
The aggregation pipeline has a full set of date operators: $year, $month, $dayOfMonth, $isoWeek, $dateToString, and more. If you need to group by hour, filter by day of week, or format dates in a report, ISODate is the path of least resistance.
Unix Timestamp: The Portable Number
The alternative is to store dates as plain numbers — milliseconds or seconds since epoch:
// Inserting as a Unix timestamp (milliseconds)
db.events.insertOne({
event: 'user_signup',
created_at: Date.now() // 1720694400000
});
This is just a NumberLong (64-bit integer) in MongoDB. No special type, no magic.
The main advantage: portability. A number is a number is a number. If you ever export your MongoDB data to a relational database, a CSV file, or a data warehouse, a timestamp column is universally understood. An ISODate string like "2026-07-11T12:00:00Z" needs parsing on every import.
The Tradeoffs in Practice
Querying
ISODate wins here. Range queries are natural:
// ISODate: clean and readable
db.orders.find({ placed_at: { $gte: startDate, $lt: endDate } });
With a Unix timestamp, the same query works but you're comparing numbers:
// Unix timestamp: works, but you need to compute the numbers
db.orders.find({ placed_at: { $gte: 1719792000000, $lt: 1719878400000 } });
Both queries use indexes efficiently. The difference is readability — startDate and endDate are self-documenting, while raw numbers require context.
Indexing
Both types can be indexed:
db.events.createIndex({ created_at: 1 });
There's no performance difference. MongoDB's B-tree index doesn't care whether the value is a number or a date. The index size is also similar — both are 64-bit values internally.
Aggregation
ISODate wins big here. The aggregation framework has rich date operators that work natively with ISODate:
db.sales.aggregate([
{ $group: {
_id: { $dateToString: { format: "%Y-%m-%d", date: "$sold_at" } },
total: { $sum: "$amount" }
}}
]);
With Unix timestamps, you'd need to convert first:
db.sales.aggregate([
{ $addFields: {
sold_date: { $toDate: "$sold_at" } // convert timestamp → ISODate
}},
{ $group: {
_id: { $dateToString: { format: "%Y-%m-%d", date: "$sold_date" } },
total: { $sum: "$amount" }
}}
]);
The conversion step works, but it's an extra stage in the pipeline and one more thing to remember.
Driver Interop
This is where Unix timestamps have an edge. When MongoDB returns an ISODate, the driver converts it to a language-native Date object. That's usually fine — until it's not.
Node.js example:
// ISODate from MongoDB
const doc = await db.collection('events').findOne({ _id: id });
console.log(doc.created_at); // Date object
console.log(doc.created_at.getTime()); // 1720694400000 — need to extract ms
If your API needs to send a numeric timestamp to the frontend, you have to convert the ISODate to milliseconds every time. With a stored timestamp, you just return the value as-is.
// Stored as timestamp: send directly to frontend
res.json({ created_at: doc.created_at }); // 1720694400000
// Stored as ISODate: need to convert
res.json({ created_at: doc.created_at.getTime() });
This isn't a dealbreaker, but it's one extra step across every route that returns dates.
Timezone Handling
Both approaches store absolute moments in time. ISODate is always UTC internally. Unix timestamps are, by definition, UTC.
The difference is in how they're displayed:
ISODate("2024-07-01T00:00:00Z")— clearly UTC, theZtells you1719792000000— no timezone visible, you just have to know it's UTC
If your team understands that all timestamps are UTC, this isn't a problem. If you're working with less experienced developers, ISODate's explicit Z helps avoid assumptions.
When to Use Which
After a few projects, here's the pattern I follow:
Use ISODate when:
- You need aggregation pipeline date operators (most apps do)
- You're building reports or dashboards that group by date parts
- Your team is familiar with MongoDB's date features
- You don't need to export data to other systems frequently
Use Unix timestamps when:
- You're building an API-first app where timestamps go straight to the frontend
- Your data frequently moves between databases or data warehouses
- You want to avoid driver type coercion issues
- You're storing dates that come from external systems already as numbers
Use both when it matters:
Some projects store both — a created_at as ISODate for queries and aggregation, and a created_at_ts as NumberLong for fast API responses. This is a denormalization tradeoff, but it's not crazy for high-traffic endpoints that serve millions of date values.
The Half-Measure: Store Timestamp, Convert When Needed
If you can't decide, store as a Unix timestamp but add a view or computed field for aggregation:
// Store as NumberLong
db.events.insertOne({
event: 'page_view',
ts: Date.now() // milliseconds
});
// When you need date-based aggregation, convert on the fly
db.events.aggregate([
{ $addFields: {
date: { $toDate: "$ts" }
}},
{ $group: {
_id: { $dateToString: { format: "%Y-%m-%d", date: "$date" } },
count: { $sum: 1 }
}}
]);
This works. The $toDate conversion is fast (it's just a type reinterpretation of the 64-bit value). But it adds verbosity to every aggregation query.
Wrapping Up
There's no universally right answer. MongoDB's ISODate is the more idiomatic choice — it leverages the database's built-in features and makes aggregation cleaner. Unix timestamps are the more portable choice — they travel between systems without conversion overhead.
If I had to pick one for a new project, I'd default to ISODate and convert to numbers at the API boundary. That gives you the best of both: clean queries and aggregation inside MongoDB, and portable numbers leaving the database.
But if you're storing dates that came from another system (like a Unix timestamp from a C# backend or a Python API), keep them as numbers. There's no benefit to converting a perfectly good timestamp into ISODate just to convert it back later.
For quick conversions between Unix timestamps and readable dates when setting up test data, the FastUnix Timestamp Converter is always handy.