`updated_at` Is Not a Conflict-Resolution Strategy

In the last post we talked about the problems with a distributed system, and touched on the fact that timestamps are not as reliable as you think they are.

If you have two updated_at fields and you compare them, how do you decide which side is the correct one?

The updated_at field only tells you that a write happened. It doesn’t tell you the meaning, or if it was intentional. Conflict resolution is fundamentally a question about causality. Did side A intend for this change to happen? A wall clock timestamp can’t tell you the answer to that.

Two independent clocks can drift, and will drift. Yes, it will get corrected by NTP occasionally. But you can’t always rely on their NTP service working. Timestamps are a fine signal that something occurred, and they’re a reasonable way for a human to sort a list and answer roughly when we think a change occurred. But if you use them as a foundation to decide what data to keep, you’re gonna end up destroying and losing data.

Things That Actually Work

The good news is the alternatives are not exotic, and you don’t need all of them.

Content hashes. Hash the meaningful content and compare hashes instead of times. This kills the metadata-edit problem outright: if the hash matches, nothing changed, no matter what the timestamp claims. It’s the highest-value change on this list and usually the easiest, because it’s a pure function of data you already have.

Version counters. A monotonic integer per record, incremented on every meaningful write. Immune to clock skew entirely, because it isn’t a clock. The cost is that somebody has to own the increment, which is straightforward with a single authority and gets harder without one.

Sync checkpoints. Record what was confirmed at the last successful sync, not just when it happened. Then the question becomes “has this changed since the last agreed state,” which is answerable, instead of “is this newer,” which is a guess.

Operation logs. Store what happened rather than only the result. Heavier, but it’s the only option that lets you reconstruct intent after the fact, and it turns “which one wins” into a question you can actually audit.

You can get most of the benefit from the first one. Hash the content, and let the timestamp go back to being a display field.

When Last-Write-Wins Is Fine

I’m not gonna lie, last write wins is often the correct engineering choice, and replacing it with something more complicated can be its own mistake. Sometimes it’s fine. If a write gets lost and the data is recoverable, that’s a trade you can live with.

If it’s a simple tool without a ton of users, adding a lot of complexity is not the way to go.

If the data is just a cache or a projection, then who cares? You can rebuild it from the authoritative source anyway.

What I’d Actually Do

Keep updated_at. It’s useful. Sort by it, display it, log it.

Just stop letting it decide things. Add a content hash and check that first, so a no-op edit stays a no-op. If a field can be written from two sides independently, give it a version counter or an explicit authority rule, and write the rule down somewhere the next person will find it.

I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].

Databases Software-development Distributed-systems Local-first Data-modeling