sehaj/learn
System design/12 Realtime, feeds and location

12.02 · concept · 9 min

Presence

Showing who's online sounds like one boolean per user. It's a constant stream of heartbeats, a timeout that decides when silence means offline, fan-out to everyone who might care, and a lot of updates nobody needs.

The problem: "Ana is viewing this page"

Snip's team plan adds a small row of avatars on a shared campaign: who else is looking at it right now, so two people don't edit the same links at once. Creators' profiles show a green dot when they're online. Simple to describe; at three million connected users, it's one of the noisiest features a system can have.

Online means "heard from recently"

A user is online if their app has a live connection. But connections die without saying goodbye: a phone goes into a tunnel, a laptop lid closes, a train enters a station. So presence can't rely on a "disconnected" event. It relies on heartbeats: the client (or its gateway) reports "still here" every 20 seconds or so, and a user whose last heartbeat is older than a timeout (say, 30 seconds) is offline.

ts
export class Presence {
  private lastSeen = new Map<string, number>(); // user -> last heartbeat time

  constructor(readonly ttlMs = 30_000, private readonly now: () => number = Date.now) {}

  heartbeat(user: string) {
    this.lastSeen.set(user, this.now());
  }

  leave(user: string) {
    this.lastSeen.delete(user); // a clean goodbye: offline at once
  }

  isOnline(user: string) {
    const t = this.lastSeen.get(user);
    return t !== undefined && this.now() - t < this.ttlMs; // silence longer than the TTL means offline
  }
}

Tested with a 30-second timeout: Ana heartbeated at 0 and 20 seconds, Ben only at 0; at 35 seconds Ana was online and Ben wasn't, and after Ana's explicit leave, nobody was. In production the same idea is one key per user in Redis set with SET presence:4821 1 EX 30 on each heartbeat: the key expiring is the user going offline.

The timeout is the same trade-off as any failure detector (08.01 Partial failure): too short and people flicker offline whenever a phone sleeps for a moment; too long and the dot stays green for a minute after they've gone.

Telling the people who care

Detecting presence is cheap. Broadcasting it is what costs:

  • Every status change goes to everyone who can see that user: followers, teammates, people on the same page.
  • A creator with 200,000 followers who reconnects every few minutes on a flaky network generates hundreds of thousands of messages for a dot that barely changed.

So presence is shaped to avoid most of those messages:

  • Only send to people looking. Subscribe to a creator's presence only while their profile is on screen, not for every follower all the time. Presence of off-screen users is fetched when needed.
  • Debounce changes. Wait a few seconds before announcing "offline", so a reconnect within that time sends nothing.
  • Coarser states for large audiences. For huge accounts, "active in the last hour" refreshed every few minutes is as useful as a live dot, and far cheaper.
  • Keep the payload tiny. Presence messages should be an ID and a state, not a profile.

Presence across many servers

Users connect to different gateways, and people watching them are on others. Either a central store holds every user's last heartbeat (simple; one more busy system), or each gateway tracks its own users and they replicate changes to each other. Phoenix's presence system takes the second route: its tracker shards use a heartbeat protocol and a CRDT, a data type whose copies can be merged in any order, to replicate presence across a cluster without a single source of truth. Either way, presence is eventually consistent (08.02 Consistency models): two people may briefly see different dots, which for a green dot is perfectly fine.

Seen in production

Phoenix

Docs

Phoenix.Tracker

Phoenix.Tracker provides distributed presence tracking. Its shards use a heartbeat protocol and a CRDT to replicate presence information across a cluster in an eventually consistent, conflict-free way, with no single source of truth or global process: each node runs trackers, and node-local changes are replicated and handled locally as diffs of joins and leaves. Phoenix's Presence builds on it and advises keeping presence metadata small and ephemeral, such as an "online" or "away" status.

What we foundPresence replicated with heartbeats and a CRDT: no single source of truth, eventually consistent, conflict-free.

Phoenix.Tracker (Phoenix.PubSub documentation)hexdocs.pm ↗

Discord

2017

Presence in guilds

When a user comes online and connects to a guild, the guild publishes their presence to all other connected sessions. With large guilds of tens of thousands of concurrent users, this kind of fan-out was where Discord's processes struggled to keep up during peak hours.

What we foundPresence is fan-out: when a user comes online, the guild publishes it to every other connected session.

How Discord Scaled Elixir to 5,000,000 Concurrent Usersdiscord.com ↗
Predict first

A creator's app reconnects every 2 minutes on a weak connection, and each reconnect broadcasts 'offline' then 'online' to 200,000 followers. What's the most effective fix?

Commit to an answer before you read on.

Recap

Interview questions

SeniorfreqMust-knowDesign

How would you implement online presence for a chat app with millions of users?

Report a mistakeEdit this page