The secrets of Firestore’s FieldValue.serverTimestamp() — REVEALED!

Doug Stevenson
Mar 30, 2020 · 8 min read

Thanks for reading past the opening graphic. It was terrible. Can we just agree I’m a bad graphic artist? Anyway.

If you’ve written any code that deals with timestamps in Firestore, you’ve almost certainly encountered the notion of “server timestamps” using FieldValue.serverTimestamp() (iOS, Android, JavaScript). And I’m willing to bet you found something to be unintuitive about it at first. But that’s OK, because there’s a lot going on behind the scenes that makes it work (or not work as you’d expect).

Make some time for server timestamps

FieldValue.serverTimestamp() is probably the most common FieldValue that you’ll encounter. There are a few different types of FieldValues, and they all act as tokens when writing Firestore document fields. These tokens don’t have a specific value on the client — they are evaluated on the server, at which point the final value is known.

When you call FieldValue.serverTimestamp(), you’ll get back a FieldValue type object that stands in for the current moment in time, as reckoned by Google. The actual value of this FieldValue token doesn’t actually contain any time data in it. You can’t get time values out of it, and you can’t do time math with it. It’s just a token. When you provide the token as the value for a field when writing a document, Firestore writes a Timestamp type value to the field with Google’s sense of the current time, at the time the write hit the server.

(Here’s a fun fact: Google servers use a special technique called leap smearing on all their servers, and they recommend this for all computer systems. This “smear campaign” is not at all libelous! What happens is this: in order to compensate for unexpected leap seconds that can be disruptive to computing that depends on steady, precise timekeeping, that extra second is “smeared” across 24 hours so high-precision clocks don’t register an abrupt shift in time. Neat, huh? Anyway, back to server timestamps…)

Here’s a bit of JavaScript that writes a timestamp to a document with path /messages/foo in field called createdAt:

const firestore = firebase.firestore()
const ref = firestore.collection('messages').doc('foo')
ref.set({
createdAt: firebase.firestore.FieldValue.serverTimestamp()
})
.then(() => {
console.log('Done')
})
.catch(error => {
console.error(error)
})

That’s looks straightforward, but there’s a couple things you might find surprising about how this works in practice:

  1. The final timestamp will have an offset from the time reckoned by the client app. Even on the fastest networks, it takes some time to send a write operation to Firestore. So, even if the client and server clocks are perfectly in sync, the final server timestamp will always appear a bit later. No attempt is made to reconcile any latency between the client and server.
  2. If persistence is enabled in the app, and the client writes the server timestamp token while it lacks network connectivity, the client will persist the token (and not the moment in time). The final timestamp will still only have a value when the client is finally able to synchronize that document. This means that if the app’s process dies before it’s able to sync, the timestamp could be delayed by days, or however long it takes for the app to get launched again and come back online.

In short, it doesn’t matter what the client does — a server timestamp only ever has a real value at the moment a request hits the server.

Listen carefully, server timestamp!

The above behavior is pretty straightforward to understand, but it can be confusing to see the actual behavior of a listener attached to the document at the time of the write. Imagine this scenario:

  1. You first attach a listener to the document at /messages/foo.
  2. You then write a createdAt field in that document with FieldValue.serverTimestamp().

The code looks like this:

const ref = firestore.collection('messages').doc('foo')// First, listen to updates to /messages/foo indefinitely
ref.onSnapshot(snap => {
const data = snap.data()
if (data) {
console.log(`timestamp: ${data.createdAt}`)
}
}
})
// Some time later, write /messages/foo with a server timestamp
ref.set({
createdAt: firebase.firestore.FieldValue.serverTimestamp()
})

If you run that, here’s what happens:

  1. The listener fires, and you get the existing value of the timestamp.
  2. The listener fires again, and you observe null for the createdAt field.
  3. The listener fires yet again, and you observe an actual timestamp value.

This is a WTF moment for anyone who’s seen this, to put it mildly. Why does it fire three times, and what’s with that null?? Don’t panic! Here’s how it works.

  1. The listener fires the first time with the initial contents of the document. (If the document doesn’t already exist, the snapshot will indicate that it’s empty.) This is expected; no problems here.
  2. Then, when set() is called, the listener fires immediately, before the write hits the server. The SDK doesn’t deliver the token value to the listener. Instead, it simply delivers null (for now), because the final value isn’t known (yet).
  3. Then, after the document gets synchronized with the server, the listener fires again with the new timestamp value determined by Firestore.

That null value in step 2 can be vexing, especially since you never actually wrote a null! If this is going to cause you some trouble, you have two ways to deal with it.

One approach is to specify SnapshotOptions when calling data() or get() on a DocumentSnapshot to say what you want to do with these missing timestamps. With this, you can ask the SDK to make an estimate for you.

The other options is to use the metadata associated with the DocumentSnapshot delivered to the listener. The snapshot metadata object contains a boolean property called hasPendingWrites, which tells you if the snapshot you’re looking at hasn’t been written to the server yet. You can use it to detect this situation, and determine if you want to do something else, such as show a local timestamp, if that’s helpful for your UI:

ref.onSnapshot(snap => {
const data = snap.data()
if (data) {
if (!data.createdAt && snap.metadata.hasPendingWrites) {
// we don't have a value for createdAt yet
const ts = firebase.firestore.Timestamp.now()
console.log(`timestamp: ${ts} (estimated)`)
}
else {
// now we have the final timestamp value
console.log(`timestamp: ${data.createdAt} (actual)`)
}
}
})

This listener behavior is intentional. The fact that the listener fires immediately on write helps you build apps that are usable while offline, and resistant to flaky mobile connections. If the client and server clocks are close, this probably won’t cause a problem for your app, in practice, as long as your code is expecting it.

Now that you understand the behavior with listeners, there’s no need to panic. Consider this article your “Hitchhiker’s Guide to Server Timestamps”, hopefully more agreeable than Vogon poetry.

How do server timestamps work with security rules?

Wow, I’m so glad you asked. That means you’re writing security rules for your app, and that’s good. Actually, it’s essential!

Security rules have a special way to let you check for server timestamps in document fields. The special value request.time contains the same timestamp that would be written by a server timestamp if the security rule allows it. Consider that document update from above that writes a server timestamp to a document:

const ref = firestore.collection('messages').doc('foo')
ref.set({
createdAt: firebase.firestore.FieldValue.serverTimestamp()
})

If you want to write a rule that requires that client apps must send the server timestamp token in the createdAt field on all writes to documents in the messages collection, you simply use this:

match /messages/{id} {
allow write: if request.resource.data.createdAt == request.time;
}

With the above rules, if the client attempts to write anything other than a server timestamp to the createdAt field, the rule will reject the write. This can be very helpful to make sure that clients don’t falsify or omit timestamps, which can be used in other security rules to limit access to the document.

For example, imagine you have a requirement for the messages collection that its documents must never be accessible to a client app for exactly one hour after the message was written. It’s easy to implement like this:

match /messages/{id} {
allow write: if request.resource.data.createdAt == request.time;
allow read: if resource.data.createdAt >
request.time - duration.value(1, 'h');
}

Notice that it’s doing a bit of date math on the request.time Timestamp object by subtracting a duration of 1 hour from it. You might find the API docs handy to learn more about durations.

The above rules do three things:

  1. Enforce that clients must write a FieldValue.serverTimestamp() value, and
  2. Enforce that clients can only get() documents whose createdAt field contains a timestamp within the past 1 hour.
  3. Enforce that clients can only issue queries against the messages collection for documented created in the past 1 hour, using the timestamp value provided as a range filter on createdAt.

Also remember that security rules are not filters, so these rules don’t let clients simply query the entire messages collection and get only those from the past hour. In order for a query to be allowed, the client has to provide a valid timestamp range filter for the createdAt field. For example, this query will always be denied, because it didn’t specify a timestamp filter for createdAt:

firestore.collection("messages").get()

However, this query provides a range filter for createdAt that intends to match the requirements of the 1-hour rule:

firestore
.collection("messages")
.where("createdAt", ">", new Date(Date.now() - 60 * 60 * 1000))
.get()

There’s one possible snag, however. If the clock on the client device isn’t accurate (that is, set too early), then the query will still be denied. You really don’t have any guarantees that the client’s clock is set correctly, even though nearly all devices synchronize their clocks with some accurate authority. Incorrect client clock times is one way that bad actors can try to hack your system, so denying the query is probably the right thing to do.

To avoid issues to client devices whose clocks can become inaccurate, perhaps adjusting the query to go 59 minutes into the past would work OK, giving a 1 minute buffer for inaccuracies. Be clear that if the provided timestamp ever goes too far back in time, the rule rejects the query, and Marty McFly might not survive. (I’m sorry, would you rather have an Avengers joke there? I’m old! Just give me this one, OK.)

Another alternative is to use an HTTP Cloud Function to make the query, where you know its clock is also managed by Google, and should be nearly perfectly in sync with Firestore. The client can then invoke the function to get the matching documents.

Note that timestamp-based queries work just fine if you provide either a Date or a Timestamp object. The Firestore SDK will convert automatically. But it’s a bit of a bummer that clients can’t query with offsets from a server timestamp, and you also can’t use a server timestamp in a query filter. If you would find that option helpful, I suggest filing a feature request to make your voice heard.

There’s no moment like now

Server timestamps are a valuable tool for your app that uses Firestore. They ensure that timestamp values for representing the current moment in time are correct, both in client code and in security rules. I hope you “seize the moment” and make the best use it in your app.

Firebase Developers

Tutorials, deep-dives, and random musings from Firebase…

Doug Stevenson

Written by

firebase-consultant.com, Firebase GDE, engineer, developer advocate, Xoogler

Firebase Developers

Tutorials, deep-dives, and random musings from Firebase developers all around the world. Views expressed are those of the authors and don’t necessarily reflect those of Firebase or its parent companies.

Doug Stevenson

Written by

firebase-consultant.com, Firebase GDE, engineer, developer advocate, Xoogler

Firebase Developers

Tutorials, deep-dives, and random musings from Firebase developers all around the world. Views expressed are those of the authors and don’t necessarily reflect those of Firebase or its parent companies.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store