Typing Indicators and Presence, Done Right
Typing indicators and presence are ephemeral signals. Keep them out of your database, drive them with short-lived state, and expire them on the server.
Typing indicators and presence dots feel trivial until you build them wrong, and then they become a source of ghosts: users shown as online who left an hour ago, a "typing" bubble that never disappears, a database quietly hammered by status writes. The fix is a mindset, not a library. These are ephemeral signals. They should never touch your durable database, they should expire on their own, and the server, not the client, should decide when a signal is stale. Get that right and both features become cheap and reliable.
Why you should never persist a typing indicator
A typing indicator is true for maybe two seconds. Writing that to a database is like writing down every twitch of someone's hand. The write volume is enormous, the data is worthless the instant it lands, and you have coupled a throwaway signal to your most expensive, most durable storage. I have seen apps where typing events were the single largest source of database writes, dwarfing the actual messages. That is backwards.
Typing state belongs in memory, on the real-time layer, with a short expiry. Client sends "I am typing" on a keystroke, the server holds that fact for a couple of seconds and broadcasts it to the room, and if no refresh arrives, it expires and the bubble vanishes on its own. Nothing is stored. Nothing needs cleanup. The signal dies naturally, which is exactly what you want from something that was only ever true for a moment.
Expire presence on the server, not on a disconnect event
The classic presence bug is trusting the disconnect event. You mark a user online when their socket connects and offline when it disconnects, and it works in testing. Then real networks show up. A phone goes through a tunnel and the socket dies without a clean close. The server never gets a disconnect. The user is shown online forever. Meanwhile a user who reconnected three times looks like they are flickering in and out.
Presence has to be driven by heartbeats and expiry, not connection lifecycle events. The client sends a small heartbeat on an interval. The server records "last seen" and treats the user as online only while that timestamp is fresh. Miss a few heartbeats and the server expires them to offline, no disconnect event required. This is the same principle as handling reconnection in real-time apps: never assume the network will politely tell you when something ended. Detect it yourself with a timeout.
I went deep on the architecture of this in how to build a presence system. The short version for this post: online is a claim with an expiration date, and the server is the one holding the clock.
Throttle the signals before they flood the room
Both features generate a firehose if you let them. A user typing a paragraph could emit dozens of events a second. Twenty people in a busy channel could each be heartbeating and typing at once. Send every raw event to every client and you have built a self-inflicted denial of service.
Coalesce and throttle. For typing, one event every second or two per user is plenty; the client should not send on every keystroke, and the server should collapse rapid repeats. For presence, batch updates so a room with churn gets one consolidated "here is who is online now" push instead of a storm of individual join and leave events. This is the same fan-out discipline that keeps live dashboard backends from melting: the number of source events is not the number of messages you should send.
Let the platform own the ephemeral layer
Here is the practical takeaway. Presence and typing are pure real-time state: short-lived, memory-resident, self-expiring, throttled. They map perfectly onto what a real-time backend does well and map terribly onto a relational database. So put them where they belong. I run these signals on the real-time layer of AltoHost so they never touch application storage, which keeps my databases sized for real data instead of for a flood of twitchy status pings. If you want the wider framing on which jobs belong to the real-time layer versus your app, I laid it out in the backend real-time apps actually need.
The test for whether you built these right is simple. Kill a client ungracefully, no clean disconnect, and watch. Within a few seconds the user should drop to offline and any typing bubble should clear, with zero rows written to your database. If a ghost lingers, you trusted the network instead of a timeout. Fix that, and typing and presence stop being the flaky little features everyone complains about and become the polish that makes an app feel alive.