Firebase and dates: why milliseconds, Timestamps, and toISOString() are not the same
If you’ve worked with Firebase for a while, this has probably happened to you at least once: a date shows up one day earlier, a time doesn’t match what you expected, or something works locally but breaks in production. Most of the time, Firebase isn’t the problem.
The problem is how we handle dates.
In this article, I want to explain—based on real-world use—the differences between the most common date formats you’ll encounter when working with Firebase: Firestore Timestamps, milliseconds since Epoch, and toISOString(). They may look interchangeable, but they’re not, and choosing the wrong one can lead to subtle bugs that are hard to track down.
How Firebase actually stores dates
Firestore doesn’t store dates the same way JavaScript does. When you save a date field in Firestore, it’s stored
internally as a Timestamp, which is made up of:
- seconds since January 1st, 1970 (the Unix Epoch)
- additional nanoseconds for precision
This format is precise, timezone-agnostic, and reliable for sorting and comparisons. Problems usually start when that Timestamp moves between different layers of your app: backend, frontend, APIs, or mobile clients.
At that point, you have to decide:
- Should I convert it to a JavaScript
Date? - Should I store it as milliseconds?
- Should I turn it into an ISO string?
That decision matters more than it seems.
Milliseconds since Epoch: simple, but easy to misuse
Milliseconds since Epoch (Date.now() or getTime()) are extremely common because they’re
just numbers. Firebase handles them perfectly well, and they’re useful for:
- ordering records
- doing time calculations
- comparing dates efficiently
The issue isn’t the format itself, but how easy it is to make mistakes with it.
Some very common ones:
- Mixing up seconds and milliseconds
- Receiving seconds from an API and treating them as milliseconds
- Converting milliseconds to dates without being clear about timezone context
Milliseconds don’t include timezone information. They represent an exact moment in time. If you later convert them to a local date without accounting for that, you can easily end up with unexpected results.
Used carefully, they’re fine. Used casually, they cause bugs.
toISOString(): probably the most misunderstood one
toISOString() returns a string in ISO 8601 format, and it is always in UTC. That detail
is often overlooked.
For example:
2026-02-08T23:00:00.000Z
That trailing Z means UTC. If you’re in Europe and you treat that value as a local time, you may suddenly be an hour—or even a day—off.
A very typical mistake looks like this:
- You store a date using
toISOString() - You read it back from Firestore
- You create a
Datefrom it in the frontend - You display it directly to the user
And suddenly the displayed date doesn’t match what you expected.
toISOString() isn’t bad. It’s actually great for APIs, logs, and data exchange. It’s just not a good
primary storage format for dates if you plan to query, compare, or manipulate them later.
Firestore Timestamp: the safest option if you let it be one
Firestore’s Timestamp type is designed for databases, not for humans. And that’s a good thing.
Its strengths are clear:
- high precision
- correct ordering
- no timezone ambiguity
- first-class support in Firestore queries
Problems usually appear when developers try to “humanize” it too early—converting it to strings before storing it, or mixing it with other formats inconsistently.
In most cases, the safest approach is:
- store dates in Firestore as Timestamps
- convert them only when you need to display them
The special case: serverTimestamp()
You might have seen FieldValue.serverTimestamp(). It’s important to know that this is not a date when you send it from the client.
It’s a "token" or instruction that tells Firestore: "When this write hits your servers, replace this field with the current server time."
Why use it?
- It avoids issues with users having incorrect system clocks (e.g., a user's phone is set to 2015).
- It ensures consistency across all writes.
Watch out: If you use a real-time listener (onSnapshot), you might receive a null value for that field immediately after writing, until the server responds with the actual timestamp (unless you configure estimateTimestamps).
This one is incredibly common:
- You store a date as an ISO string
- You read it back and do
new Date(isoString) - You format it for display
- The user sees the previous day
Nothing is broken. The date is in UTC, your browser is in local time, and the conversion crosses a day boundary. That’s it.
But if you don’t expect it, it can take hours to figure out why.
Comparison: Date Formats in Firebase
| Format | Type | Timezone | Best for... |
|---|---|---|---|
| Firestore Timestamp | Object | UTC (Agnostic) | Storage, Querying, Sorting |
| serverTimestamp() | Sentinel (Token) | UTC (Server) | createdAt, updatedAt fields |
| Milliseconds | Number | UTC (Agnostic) | Calculations, Performance |
| ISO String | String | UTC (if Z) | APIs, JSON, Logging |
What actually works in practice
There’s no single “correct” format, but there are definitely combinations that cause trouble. After dealing with this more times than I’d like to admit, this is what I recommend:
- Store dates in Firestore as Timestamp
- Use milliseconds when you need fast calculations or compatibility
- Avoid ISO strings as your main storage format
- Convert dates to human-readable form only in the presentation layer
- Always be explicit about whether you’re working in UTC or local time
If you’re unsure which format to use, the real question is usually not “which one is better?”, but where in the data flow you are.
One last practical note
When you’re debugging logs, integrations, or historical data, being able to quickly tell whether a value is seconds, milliseconds, ISO, or a Timestamp saves a lot of time and frustration. Small conversion mistakes are responsible for a surprising number of production bugs.
Final thoughts
Firebase doesn’t make working with dates harder. The confusion comes from assuming that all date formats mean the same thing. They don’t.
Understanding the difference between Firestore Timestamps, milliseconds since Epoch, and toISOString()
helps you avoid silent errors, strange offsets, and bugs that only show up after deployment.
And in real projects, that understanding makes a real difference.