Quantum Leap: Oh boy, which device am I on? What last-write-wins loses when resuming playback
The one conflict rule behind BoxOffice and Billboard library sync, the four things it gives up, and the version column that rejected a real seek.

TL;DR. BoxOffice and Billboard sync library state and resume points with one rule: a write carries the time the user made it, the newest wins, ties apply, a clock from the future is clamped to server time, and a stale write gets a 200 so retry queues drop it. It is simple and live on web, and it loses four specific things, which you should choose to lose rather than discover.
Series: Listening together, miles apart
- NSYNC: What does in sync actually mean? Four consistency promises for multi-device state (coming 23 Nov)
- Dungeons & Dragons: Who rolls the dice? Seeded shuffle versus local shuffle across a handoff (coming 24 Nov)
- Tetris: Every piece fits, so why no line clear? WebRTC calls still down after three fixes (coming 25 Nov)
- This post
- Fantastic Four: Four kinds of state, four sync rules. A reference card for multi-device playback (coming 27 Nov)
The short version
Last-write-wins is an honest rule when losing the older of two edits is what the user would expect, and a dishonest one when it is not. Its losses are predictable: slow clocks, sub-second races, 'furthest' versus 'latest', and anything that should have merged. Write them down, then decide per field.
Background
I build a music streaming platform (Billboard), a video service (BoxOffice), a dating app with real-time chat, and the services between them, on web and iOS. One person, a small real user base.
On 2026-09-26 both products got the same rule, word for word: BoxOffice in migration 0021, Billboard in migration 0039. It is live on the server and the web client. The iOS side is written on a branch and has not shipped, so today the devices racing each other are mostly browsers.
Scope
What this covers
- The rule, clause by clause, and why each clause exists
- Four things it loses, and one bug in my first version
- Resume points and library membership
What it does not
- The shared play queue: it uses versions and conflicts instead (previous post)
- CRDTs and merge-based sync: not built here
- Spotify's sync internals, which I have not read about
The challenge
A resume point is a register: one value, many writers. The phone saves 12:00, the laptop saves 41:10, and a write that sat in an offline queue arrives an hour after it happened. Which one is true? Server arrival order is wrong, because the late write is the old one. So the write carries the client's time. Now the answer depends on every device's clock, and a device whose clock runs fast wins every argument for as long as it is fast.

Solutions and process
- THE RULE. Time comes from the body's updatedAt (or ?updatedAt= on a bodyless DELETE, because a custom header would need CORS). Absent means server now, so old clients keep working. A time in the future is clamped to server now. Ties apply, so the later arrival wins a tie. Removals leave tombstones that never move backwards. A stale write gets 200 with stale: true and the current value, never a 4xx, so a retry queue drops it instead of retrying for ever.
- WHAT DID NOT WORK FIRST. BoxOffice's first version compared against
last_accessed_at, a column that /play bumps with the server's clock. A device whose clock ran slightly behind had its first seek rejected as stale, because merely pressing play had moved the version past it. The fix was a dedicatedlww_atcolumn that only client change times ever write. - LOSS ONE, SLOW CLOCKS. The server clamps fast clocks but cannot correct slow ones. The web client stamps plain device time because a browser cannot read the Date header cross-origin, so a slow laptop loses races it should win by as much as it is slow. iOS corrects skew from response Date headers, on the branch that has not shipped.
- LOSS TWO, SUB-SECOND RACES. BoxOffice stores the version at one-second resolution, so two writes in the same second land in arrival order. Billboard uses milliseconds. LOSS THREE, LATEST IS NOT FURTHEST. A resume point takes the newest write, not the furthest position: rewatch the opening on your phone after finishing on the laptop and your resume point moves back. Usually right, sometimes not. LOSS FOUR, NOTHING MERGES. A playlist renamed on two devices keeps one name; Billboard versions playlist fields separately so a rename on one device does not undo a description edit on another, but two renames still collide.
- THE EVIDENCE. BoxOffice's rule was proven against production with a QA account across 12 races. Billboard's has 74 checks against real SQLite, and in production only unauthenticated rejections and public reads were smoke tested. An authenticated write race on Billboard in production is still unproven.
How Billboard reads a client's change time. Real code, billboard_users/src/lww.ts.
/** Parse a client's change time. Returns server `now` when absent, unreadable or in the future. */
export function parseClientTime(raw: unknown, now: number): number {
let ms: number | null = null;
if (typeof raw === "number" && Number.isFinite(raw)) {
// Seconds would read as 1970; nothing in this product predates 2001 in ms.
ms = raw < 1e11 ? raw * 1000 : raw;
} else if (typeof raw === "string" && raw.trim() !== "") {
const s = raw.trim();
if (/^\d+(\.\d+)?$/.test(s)) {
const n = Number(s);
ms = n < 1e11 ? n * 1000 : n;
} else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/.test(s)) {
// SQLite CURRENT_TIMESTAMP: zoneless, and UTC. Date.parse would read it as local time.
ms = Date.parse(s.replace(" ", "T") + "Z");
} else if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(s)) {
ms = Date.parse(s);
}
// An ISO string with no zone is ambiguous across devices: treated as unreadable.
}
if (ms === null || !Number.isFinite(ms) || ms <= 0) return now;
return Math.min(Math.floor(ms), now);
}/** Parse a client's change time. Returns server `now` when absent, unreadable or in the future. */
export function parseClientTime(raw: unknown, now: number): number {
let ms: number | null = null;
if (typeof raw === "number" && Number.isFinite(raw)) {
// Seconds would read as 1970; nothing in this product predates 2001 in ms.
ms = raw < 1e11 ? raw * 1000 : raw;
} else if (typeof raw === "string" && raw.trim() !== "") {
const s = raw.trim();
if (/^\d+(\.\d+)?$/.test(s)) {
const n = Number(s);
ms = n < 1e11 ? n * 1000 : n;
} else if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(\.\d+)?$/.test(s)) {
// SQLite CURRENT_TIMESTAMP: zoneless, and UTC. Date.parse would read it as local time.
ms = Date.parse(s.replace(" ", "T") + "Z");
} else if (/[zZ]|[+-]\d{2}:?\d{2}$/.test(s)) {
ms = Date.parse(s);
}
// An ISO string with no zone is ambiguous across devices: treated as unreadable.
}
if (ms === null || !Number.isFinite(ms) || ms <= 0) return now;
return Math.min(Math.floor(ms), now);
}

Takeaway
- Version with a dedicated column that only client change times write. Never a timestamp the server bumps.
- Clamp future clocks. Correct slow ones from the server's Date header where you can read it.
- Answer a stale write with 200 and the current value, so retry queues drop it.
- The cost: slow clocks, same-second races, latest over furthest, and no merges. Choose them on purpose.
Where this meets Spotify's engineering
Listening Together, Miles Apart: Every second more than 30,000 people around the world press play on the same song, and Spotify samples those plays and groups them by time window to find two listeners in the same moment.
Both problems turn on what 'the same moment' means across clocks you do not control; they window a sampled global stream, I compare one person's device clocks with a clamp, and mine is a far smaller and simpler version of the question.
Thanksgiving, 2026-11-26. A lot of people will finish a film on someone else's TV this week and pick it up on their own phone later, which is exactly the handoff this rule decides.
Over to you
Engineers who own 'continue watching': do you keep the newest position or the furthest one, and how did you decide?
References
Next: the four sync rules on one card, with what each one costs and which are live.
More: LinkedIn · Instagram. Portfolio and case studies: designsbyduhart.org.
If any of this saved you an afternoon, Buy me a coffee.