Presence is a real-time signal of whether a user is currently online, away, or offline, kept fresh by connections and heartbeats. The green dot is deceptively simple UI over a genuinely hard distributed problem: the server must infer absence — devices rarely announce their death — and then broadcast every inference to everyone watching, at contact-list scale. Standardized long before modern chat, in XMPP’s presence stanzas, the pattern’s fundamentals haven’t changed since.
Key takeaways
| Question | Answer |
|---|---|
| The states | online · away/idle · offline — plus user-set overrides (do not disturb) |
| The mechanism | Heartbeat writes + TTL expiry; connection events as the fast path |
| The numbers | ~30 s heartbeat · offline timeout at 2–3× the interval · 30–60 s flap grace |
| The companion | Last seen — the durable timestamp behind the ephemeral dot |
| The hard part | Fan-out: N watchers × M status changes, multiplied by your success |
The heartbeat contract
The entire pattern fits in one exchange:
client every 30 s: save { status: "online", lastActiveAt: now }
server every 60 s: sweep — lastActiveAt older than 90 s? → status: "offline"
(TTL = 2–3 × heartbeat interval)
watcher status = "online" → green dot
status = "offline" → "last seen 12:41" ← lastActiveAt, the durable part
The same contract as SDK code — heartbeat out, subscription in:
// JavaScript / Node.js — Back4app JS SDK
// Presence: heartbeat your own status, subscribe to your contacts'
const heartbeat = () =>
myPresence.save({ status: 'online', lastActiveAt: new Date() });
await heartbeat();
setInterval(heartbeat, 30_000); // server sweeps offline at 2–3× this
const contacts = new Parse.Query('Presence');
contacts.containedIn('user', myContactIds);
const sub = await contacts.subscribe();
sub.on('update', (p) => setStatus(p.get('user'), p.get('status'))); // Flutter / Dart — Back4app Flutter SDK
// Presence: heartbeat your own status, subscribe to your contacts'
Future<void> heartbeat() async {
myPresence
..set('status', 'online')
..set('lastActiveAt', DateTime.now());
await myPresence.save();
}
Timer.periodic(const Duration(seconds: 30), (_) => heartbeat());
final contacts = QueryBuilder<ParseObject>(ParseObject('Presence'))
..whereContainedIn('user', myContactIds);
final sub = await LiveQuery().client.subscribe(contacts);
sub.on(LiveQueryEvent.update, (p) => setStatus(p.get('user'), p.get('status'))); // iOS / Swift — Back4app Swift SDK
// Presence: heartbeat your own status, subscribe to your contacts'
func heartbeat() async throws {
myPresence.status = "online"
myPresence.lastActiveAt = Date()
_ = try await myPresence.save()
}
// Fire every 30 s — the server sweeps offline at 2–3× this
let contacts = Presence.query(containedIn(key: "user", array: myContactIds))
let sub = try await contacts.subscribe()
sub.handleEvent { _, e in
if case .updated(let p) = e { setStatus(p.user, p.status) }
} // Android / Kotlin — Back4app Android SDK
// Presence: heartbeat your own status, subscribe to your contacts'
fun heartbeat() {
myPresence.put("status", "online")
myPresence.put("lastActiveAt", Date())
myPresence.saveInBackground()
}
timer.scheduleAtFixedRate(0, 30_000) { heartbeat() } // sweep at 2–3× this
val contacts = ParseQuery.getQuery<ParseObject>("Presence")
contacts.whereContainedIn("user", myContactIds)
val sub = ParseLiveQueryClient.Factory.getClient().subscribe(contacts)
sub.handleEvent(SubscriptionHandling.Event.UPDATE) { _, p ->
setStatus(p.getString("user"), p.getString("status"))
} Connection events vs. heartbeat TTL
The two detection models, compared honestly — no ranking page does this in one table:
| Connection lifecycle | Heartbeat + TTL | |
|---|---|---|
| Online signal | Socket opened | Fresh heartbeat within window |
| Offline signal | Close event / close frame | TTL expiry — no heartbeat for 2–3 intervals |
| Detection speed | Instant on clean close | Bounded delay (up to the timeout) |
| Silent failures | Missed — power loss sends no close frame | Caught — silence is the signal |
| Server state | Per-connection tracking | Stateless writes to a TTL store |
| Fails how | Zombie “online” users | Brief false “offline” on missed beats |
The WebSocket protocol itself teaches the lesson: it ships ping/pong control frames precisely because TCP won’t reveal a vanished peer, and its close-code taxonomy reserves 1006 for “abnormal closure — no close frame received,” the zombie case. Production presence therefore layers both models: connection events for instant transitions, heartbeat expiry as the truth that catches what events miss.
The state machine
Two refinements distinguish polished implementations. Away is client-detected: the server can’t see a backgrounded tab, but the browser can — the Page Visibility API flags hidden tabs, input listeners flag idleness, and a best-effort offline signal on tab close rides the Beacon API. Offline is debounced: the grace state exists because mobile connections flap — publish “offline” on every tunnel and elevator, and every watcher’s contact list flickers in sympathy. Delay the announcement, cancel on reconnect, and flapping collapses into stillness. One more distinction worth stealing from the protocol that formalized presence: computed presence (online, by connection) versus user-set status (do not disturb) — the latter always wins the merge.
Ephemeral by design
Presence is a cache of reality, not a record: a status that arrives late is worse than none — yesterday’s “online” is a lie today. That principle drives the implementation choices: statuses live in fast stores with TTLs rather than durable tables, are never queued for offline delivery, and are the first data dropped under backpressure. Only lastActiveAt deserves persistence. Typing indicators are the principle at its extreme — micro-presence scoped to one conversation, expiring in seconds, debounced at the sender, and discarded rather than retried; if “Ada is typing…” can’t arrive now, it should never arrive.
The fan-out problem
Presence’s scaling wall is multiplication, not storage. A worked example: 100,000 concurrent users, each visible to 50 contacts, each transitioning state a modest 20 times per hour — that is 100,000 × 50 × 20 = 100 million presence events per hour to deliver, from a feature that stores one small row per user. The levers, in the order to pull them: subscribe narrowly — watch the users on screen (the open conversation list), not the full contact graph; pull, don’t push, the long tail — fetch last-seen when a profile opens instead of streaming it; cap broadcast in groups — beyond a few hundred members, show presence on interaction, not for the roster; and batch transitions so a flapping user costs one debounced event, not thirty. Fan-out itself rides the standard pub/sub machinery — presence is that pattern’s at-most-once corner case, where dropping a stale event is a feature.
Multi-device and privacy
One user, three devices, one dot — presence per user is a merge over presence per device. Track each device’s heartbeat separately; the user is online while any device is, and the merge policy is most-available-wins: online on the phone beats idle on the desktop, and any device’s do-not-disturb overrides the rest. Last-seen reports the most recent device. Privacy is the other half of the design, and the half engineering blogs skip despite being what users most ask about: visibility rules (everyone / contacts / nobody), reciprocity (hide yours, lose sight of others’), and coarse last-seen (“recently” instead of timestamps). Enforce the rules in the data layer’s permissions — presence is behavioral data, and a leaked “online at 3 a.m.” is a real disclosure.
Common use cases
- Chat and messaging — the canonical green dot, last seen, and typing indicators.
- Collaboration tools — who’s in the document, cursor presence, active-now sidebars.
- Support and marketplaces — agent availability routing, “seller is online” trust signals.
- Multiplayer and social — lobbies, friend lists, join-your-friend affordances.
- Workforce tools — availability states feeding routing and status dashboards.
How should you build presence? A decision matrix
| Feature | Mechanism |
|---|---|
| Green dot on contacts | Heartbeat + TTL store, live-query subscription on visible users |
| Instant transitions in an open chat | Connection events as fast path over the socket |
| Last seen | Timestamp on every heartbeat; pull on demand |
| Away detection | Client-side: visibility + idle listeners, reported in the heartbeat |
| Typing indicator | Per-conversation micro-events, seconds-long TTL, never persisted |
| Presence in a 5,000-member group | Don’t broadcast — show on interaction, cap the roster |
Limitations and trade-offs
- Presence is probabilistic. Between heartbeats, the dot is a guess; honest systems embrace bounded staleness rather than pretending to instant truth.
- Freshness costs writes. Halving the heartbeat interval doubles write load across every online user — detection speed is purchased in throughput.
- Fan-out scales with success. The feature is cheap at 1,000 users and an architecture project at 10 million; design the subscription scope before growth forces it.
- Flapping is inherent. Mobile networks guarantee reconnect churn; without debouncing, presence amplifies network noise into UI noise.
- It is surveillance-shaped. Online patterns reveal sleep, work, and habits; visibility controls and permission-layer enforcement are requirements, not enhancements.
Presence on Back4app
Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. The building blocks above map one-to-one: a Presence class holds status and lastActiveAt; the heartbeat is a periodic save (the code tabs); watchers hold a Live Query subscription on the contacts currently on screen, receiving pushed updates over the platform’s managed WebSocket fleet; a scheduled Cloud Code job is the TTL sweep, flipping stale rows to offline and firing the debounce; and class-level permissions plus ACLs implement the visibility rules in the data layer, where they belong. Nothing bespoke to operate — presence becomes a data-modeling exercise over infrastructure the backend already runs.
Frequently asked questions
What is a presence system?
A service that tracks whether each user is currently online, away, or offline and broadcasts changes to the users allowed to see them, in real time. Under the hood: persistent connections or periodic heartbeats writing to a fast in-memory store, with status changes fanned out over pub/sub.
How do apps know you are online?
Two signals, usually combined: the act of holding an open real-time connection (socket open means present), and periodic heartbeats sent over it. When heartbeats stop for longer than a timeout window — typically two to three intervals — the server marks you offline, even if the connection never formally closed.
How does "last seen" work?
The server records a timestamp on every heartbeat and disconnect. While you are online, the live status is served; once you go offline, the last recorded timestamp answers instead. It is the one durable piece of an otherwise ephemeral system — status expires, last-seen persists.
What heartbeat interval should presence use?
Thirty seconds is the common production default, with the offline timeout at two to three times the interval (60–90 seconds). Faster intervals shrink detection delay but multiply write load linearly with your online population; the interval must also stay below infrastructure idle timeouts, or proxies kill quiet connections first.
Connection-based or heartbeat-based detection — which is better?
Both, for different failures. Connection events (open/close) give instant transitions but miss silent deaths — a device that loses power sends no close frame, leaving a zombie connection. Heartbeat expiry bounds that staleness at the cost of detection delay. Production systems use connection events as the fast path and heartbeat TTL as the source of truth.
How do you stop online status from flickering?
Debounce the offline transition: on disconnect, start a grace timer — commonly 30 to 60 seconds — and cancel it if the user reconnects, publishing "offline" only when the timer fires. Users on elevators and train tunnels reconnect constantly; without the grace period, every contact list they appear on flickers with them.
How does presence scale to millions of users?
By respecting the fan-out math: every status change must reach every watcher, so N contacts times M transitions explodes fast. The levers are an in-memory TTL store for status, subscribing only to visible users (the open chat list, not the full contact book), pulling last-seen on demand instead of pushing it, and capping status broadcast in large groups.
How does multi-device presence work?
Track a connection or heartbeat per device and merge: the user is online while any device is, offline only when the last one goes quiet. The usual merge policy is most-available-wins — online on the phone beats idle on the desktop — with last-seen taken from the most recent device.
Can users hide their online status?
They should be able to — visibility is a product feature, not an afterthought: rules like everyone, contacts-only, or nobody, usually with reciprocity (hide yours and you cannot see others'). Presence data is behavioral data; scope who can watch whom in the permission model, not in the UI.