How it works
Live cursors: how presence works in collaborative apps
Presence data is the opposite of document data in almost every respect, and treating them the same is a classic mistake.
Coloured cursors drifting around a shared document look like a small cosmetic feature bolted onto the real work of merging edits. They are neither small nor cosmetic. Presence is a second system, running beside the document with rules that invert almost every rule the document follows, and it does at least as much work in making collaboration feel sane.
Two systems, opposite requirements
A collaborative document is built to never lose anything. Every keystroke is durable, ordered, and replayable; deletions leave tombstones behind precisely so that history stays intact.
Presence wants none of that.
| Document state | Presence state | |
|---|---|---|
| Lifetime | Permanent | Meaningless once the user disconnects |
| Dropped update | Data loss — unacceptable | A cursor is 60ms stale — fine |
| History | Essential | Actively harmful to keep |
| Update rate | Keystrokes | Every mouse move, if you let it |
| Correct model | Append-only log | Last-write-wins per user |
Last-write-wins is the wrong answer for documents and exactly the right one for presence. Nobody needs the cursor position from four seconds ago; the newest one supersedes it completely. That single difference drives everything else.
The classic implementation mistake is to store presence in the document — a cursors map inside the shared data structure. It works in a demo and degrades badly in practice: mouse movement generates dozens of updates per second, each one permanently appended to a structure designed never to forget. Within an hour the document’s history is almost entirely records of where somebody’s cursor used to be. Presence belongs in a separate channel with separate storage semantics.
What a presence record contains
Typically small and entirely disposable:
- A per-connection identifier — not a user ID; the same person in two tabs is two presences.
- A display name, and a colour derived deterministically from the ID so it is stable across a session.
- The cursor position, and the selection range if one exists.
- A liveness timestamp.
The colour detail is worth a sentence. Deriving it by hashing the connection ID into a fixed palette means every client independently computes the same colour for the same person, without any negotiation, and the colour stays put for the whole session — which is what makes “the green cursor” a usable thing to say out loud.
Anchoring a position in a moving document
Here is the subtle part. A cursor at offset 400 is only at offset 400 in one particular version of the document. If a collaborator inserts a paragraph above, everything below shifts, and a naively broadcast integer now points somewhere meaningless. Every remote cursor drifts a little further wrong with every edit.
This is the same instability that breaks naive collaborative editing, and it has the same solution. Rather than broadcasting an index, translate the position into a relative position anchored to the identity of a specific character in the document — “immediately after the character with ID (A,412)”. That description survives arbitrary edits elsewhere. Receiving clients convert it back into a concrete offset against their own copy at render time.
The conversion runs after every document change, so a cursor stays glued to the text it was next to even while text above it is rewritten. When the anchor character is itself deleted, the position resolves to the nearest surviving neighbour, which is the intuitively right behaviour: the cursor collapses to where the deleted text used to be.
Keeping the channel quiet
Presence generates far more events than a document does, and almost all of them are worthless. Three techniques do the necessary work.
Throttle rather than debounce. The distinction matters here. Debouncing waits for movement to stop before sending, which means a cursor in continuous motion transmits nothing at all — the exact case you most want to show. Throttling sends at most one update per interval regardless. Somewhere around 50–100ms is the range where motion still reads as smooth and traffic stays sane.
Interpolate on the receiving side. Updates arriving every 80ms will look like teleportation if rendered literally. Animating between the last two known positions over roughly the update interval turns discrete samples into apparently continuous motion, and costs one CSS transition. Almost all of the perceived smoothness of a good presence implementation comes from this, not from a higher update rate.
Suppress no-ops. Editors fire selection events constantly, including for selections that did not change. Comparing against the last sent value before transmitting removes a surprising proportion of all traffic.
Knowing when someone has left
Disconnections are mostly not graceful. Tabs are closed, laptops are shut, trains enter tunnels. If presence is only removed on a clean goodbye, a shared document accumulates ghosts — cursors belonging to people who left an hour ago, frozen mid-line.
Two mechanisms, used together:
- Server-side disconnect handling. The realtime backend registers an instruction, at connection time, to clear this connection’s presence record when the socket drops. It fires whether the client said goodbye or vanished.
- Client-side timeout. Each presence carries a heartbeat timestamp; any record not refreshed within a window — 30 seconds is typical — is treated as gone. This is the backstop for the case where the socket stayed open but the peer stopped participating.
The belt-and-braces approach exists because the two failure modes are different. A dropped socket is caught immediately by the first; a wedged client that holds its connection open is only caught by the second.
Why any of this matters
It is tempting to file presence under polish. It is closer to being the mechanism by which collaboration works at all.
A CRDT guarantees convergence — everyone ends up with the same document. It does not guarantee anyone wanted that document. Two people simultaneously rewriting the same function will get a deterministic interleaving of both rewrites, which is a correct merge and a useless result.
Presence is what stops that happening, and it does so socially rather than algorithmically. Seeing a colleague’s cursor sitting in the function you were about to change makes you go somewhere else, or say something. The conflict is avoided before it is created, by a human being reacting to information. No merge algorithm can do that, because by the time the algorithm is involved both edits already exist.
This is also why presence carries so much of the felt quality of a collaborative tool. Pairing remotely works largely because both people can see where the other is looking, which is the thing a screen share destroys and a shared editor restores. Watching a cursor move is the remote equivalent of watching someone point at your monitor.
In NoobProMax, presence rides on Firebase Realtime Database alongside the document updates but in a separate, ephemeral namespace with server-side disconnect cleanup — throttled, interpolated, anchored to relative positions, and never written into the document’s own history.
Read next
How CRDTs make real-time collaboration possible
Two people type on the same line at the same moment and neither edit is lost. Here is the data structure that makes that work, explained without the algebra.
Remote pair programming: a practical guide
Driver-navigator, ping-pong and strong-style pairing, what each is good for, and how to run them over a video call without either person going quiet for twenty minutes.
Operational transformation vs CRDTs
Two ways to let many people edit one document. One needs a central server and a lot of careful case analysis; the other needs neither. What each actually costs.