Cursor State
{
user_id,
user_name,
color,
position: { line, col } // for text, or { x, y } for canvas
selection: { anchor, head },
last_updated
}
Ephemeral Storage (Redis)
Cursor state stored only in Redis, not DB. Cursor positions change many times per second and have no lasting value once a user leaves, so writing them to a database would pile up load for data that is stale within seconds. Interviewers probe this trade-off — be ready to explain why the TTL, rather than an explicit cleanup job, is what keeps dead cursors from lingering after a node crash.
HSET cursors:{doc_id} user_id {serialized_cursor_state}
EXPIRE cursors:{doc_id} 30s -- refreshed on each cursor move
Broadcast
Cursor move event → publish to Redis channel doc:{doc_id}:cursors → all WebSocket nodes push to connected clients. Each node only forwards to the clients it holds open, so the channel does the fan-out and no node needs to know the full connection list — this is what lets you add WebSocket nodes horizontally without changing the broadcast path.
Debounce: client sends cursor updates at most every 50ms. Without debouncing, a fast drag can emit hundreds of events per second per user, multiplied across every collaborator on the document. Interviewers want you to bound outbound traffic on the client before it hits the network — 50ms (about 20 updates/second) stays visually smooth while cutting event volume by an order of magnitude.
Awareness Protocol
On join: client fetches all active cursors (HGETALL cursors:{doc_id}) and subscribes to channel. This snapshot-then-subscribe pattern mirrors the real-time presence model used in chat apps: read current state once, then keep it live with a stream. Watch the ordering — subscribe before the HGETALL, otherwise a cursor that moves during the fetch is lost.
On disconnect: remove user from hash (HDEL) → broadcast cursor-leave event. Detecting the disconnect is the hard part: a dropped WebSocket may not fire a clean close, so nodes lean on the 30s Redis TTL as a backstop and a heartbeat to catch silent drops sooner.
Color Assignment
Deterministic color from hash of user_id (16 distinct colors). Deterministic means no coordination and no storage — every node computes the same color for a user independently. The follow-up interviewers like: with only 16 colors, two users can collide on the same document, so pair the color with the name tag (or widen the palette) to keep them distinguishable.
UI Features
Selection
Highlight range in collaborator color. Render the selection as a semi-transparent overlay so the underlying text stays readable, and clamp the range to the current document state — a classic bug is a stale selection stretching past text another user just deleted.
Name Tag
Floating label near cursor showing user name. Position it above the caret and flip it below when near the top edge so it never clips off-screen. To avoid a wall of overlapping labels, fade tags after a couple seconds of inactivity and show them again on movement or hover.
Ghost Cursor
Show last known position faded when user is idle (no movement in 10s). The ghost bridges the gap between “active” and “gone” — it tells collaborators where someone left off without cluttering the view with a solid cursor. Distinguish it clearly from the disconnect case: idle keeps the faded cursor, while a cursor-leave event removes it entirely.
See also: Atlassian Interview Guide
See also: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering
Keep sharpening your system design:
