How to Build a Presence System That Feels Live
A presence system tracks who is online in real time. Here is what building presence actually requires, why it is harder than it looks, and how to keep it accurate.
Presence is the little green dot that says someone is online. It looks like the simplest real-time feature you could build. It is not. Presence is a distributed consensus problem wearing a friendly costume, and the gap between a demo that works on your laptop and a presence system that stays accurate under real load is enormous.
Here is the honest version of what building presence requires, and why most first attempts drift into a field of ghosts within a week of launch.
What presence actually has to answer
A presence system answers a deceptively hard set of questions. Who is online right now. On which device, because the same person can be on a phone and a laptop at once. When did they go idle. When did they truly disconnect, as opposed to briefly blink offline. And it has to answer all of this fast enough to feel live, without flooding the network with updates every time someone twitches.
Each of those is its own small problem. Online versus idle versus offline is a state machine, not a boolean. Multi-device means you are tracking sessions, not users, and rolling them up. Fast enough to feel live but not so fast it floods is a rate-limiting decision you have to make deliberately.
The naive build treats presence as a flag you set on connect and clear on disconnect. That version breaks the first time a connection dies without a clean close, which happens constantly on real networks.
The ghost connection problem
Sockets lie. A phone drops into a tunnel and the TCP connection is dead, but your server has no idea for thirty seconds or more. If your only signal for offline is the close event, you now show a user online who left for the day. Multiply that across thousands of connections and your presence data is mostly ghosts.
The fix is that presence must be pull-verified, not push-assumed. You do not trust that a connection is alive because nobody told you it closed. You require proof of life.
That proof comes from heartbeats. The client sends a small ping on an interval. The server records a short-lived presence record with an expiry. If the heartbeat stops arriving, the record ages out on its own and the user drops offline. You are not waiting for a close event that may never come. You are treating silence as death after a timeout, which is the only model that survives real networks.
Heartbeats and expiring state
The pattern that works is presence records with a time to live, renewed by heartbeat. A user's presence record is written with an expiry of, say, thirty seconds. Every heartbeat renews it. Stop heartbeating and it expires. This is the same discipline that keeps WebSocket clusters honest at scale: short-lived state that must be actively renewed rather than long-lived state you hope is still true.
The tradeoff is latency versus load. Short expiry means fast, accurate offline detection but more heartbeat traffic. Long expiry means less traffic but staler data. There is no universal right answer. There is a right answer for your product, and picking it is a design decision, not an accident.
Across a fleet of servers this gets harder, because every node contributes its local view to a shared picture. The presence store has to merge those views and expire cleanly regardless of which node held the connection. Getting that consistent is exactly the kind of thing I would rather build once in a governed foundation every product inherits than rebuild per app.
Why I would not hand-roll this per product
Presence has the same guarantees whether it powers a collaborative editor, a support chat, or a live ops dashboard. The messages differ. The mechanics do not. Who is online, on which device, verified by heartbeat, expired on silence, merged across nodes. That is a fixed set of problems.
Rebuilding it per product is how you end up with three subtly different presence systems, each with its own ghost bug and its own on-call story. This is why presence belongs in the real-time backend itself. AltoHost is built to own presence as a first-class guarantee alongside connection lifecycle and message delivery, so the application on top can just ask who is online and trust the answer. It fits the broader case I make for infrastructure you actually control: own the hard part once, then stop paying for it.
Closing
Presence is a consensus problem, not a flag. Build it on heartbeats and expiring records, treat silence as offline, and decide the latency-versus-load tradeoff on purpose. Do that and the green dot tells the truth. Skip it and you ship a feature that lies to every user, quietly, all day long.