Designing Typing Indicators & Online Presence at Massive Scale

Designing Typing Indicators & Online Presence at Massive Scale

Designing Typing Indicators & Online Presence at Massive Scale

A complete, interview-depth system design walkthrough for building the “is online / is typing” features behind a messaging platform serving hundreds of millions of concurrently connected users — covering persistent connections, TTL-backed presence leases, pub/sub fan-out, and the bounded push-vs-pull trade-offs at extreme scale.

01

Introduction & History

Two of the smallest-looking features in any messaging app — the green dot next to a contact’s name, and the “typing…” bubble that appears while someone composes a reply — are, underneath the surface, among the hardest real-time distributed systems problems a messaging platform has to solve. They look trivial because the UI is trivial: a dot, a label, three animated dots. But building them correctly for hundreds of millions of users, each holding open a persistent connection that can drop at any moment, on flaky mobile networks, across multiple devices per person, is a genuinely difficult engineering problem that touches almost every hard topic in distributed systems: connection management at extreme scale, fan-out, eventual consistency, heartbeats, and the CAP theorem in a very concrete, user-visible way.

In the early days of instant messaging — think desktop clients like ICQ, MSN Messenger, and early Yahoo Messenger in the late 1990s and 2000s — presence was relatively simple because scale was smaller and clients were mostly desktop applications with stable, long-lived TCP connections. A single presence server could track a few million users without much trouble. As messaging moved to mobile — with users constantly switching between Wi-Fi and cellular, backgrounding apps, losing signal in elevators and tunnels — presence became dramatically harder. A “connection” is no longer a reliable signal of “the user is present”; a mobile app can hold a socket open while backgrounded, or can silently drop a connection without either side immediately knowing. Group chats and large channels made the fan-out problem — telling everyone in a 500-person group who else is currently typing — a genuine scaling challenge, not just a broadcast to a handful of friends.

Modern messaging platforms (WhatsApp, Messenger, Slack, Discord, Telegram) serve hundreds of millions to billions of users, many of whom are connected simultaneously at any given moment, and they treat presence and typing indicators as first-class, heavily-optimized subsystems — not afterthoughts bolted onto the chat backend. This tutorial designs such a system from first principles: how to track “online,” “away,” and “last seen,” how to propagate an ephemeral “typing…” signal to the right people within tens of milliseconds, and how to do all of this without melting your database under the write load of hundreds of millions of users whose online status changes constantly.

It’s worth spending a moment on why this specific pair of features gets so much dedicated engineering attention at large messaging companies rather than being treated as a minor UI flourish. Presence and typing indicators are, in aggregate, by far the highest-frequency events flowing through a messaging platform’s real-time infrastructure — a typical user sends only a handful of actual messages per session, but their client sends dozens of heartbeats and potentially dozens of typing events in the same window. If this traffic were routed through the same systems built to guarantee reliable message delivery, it would dominate capacity planning and cost for a feature that, unlike message delivery, has no correctness requirement at all — nobody has ever filed a support ticket because a typing indicator was one second late. That asymmetry — extremely high volume, extremely low value-per-event, zero durability requirement — is exactly what makes this such a distinctive and interesting system design problem, because it rewards a completely different set of engineering instincts than the “make every write durable and consistent” instincts that dominate most backend systems.

Everyday analogy

Think of a large open-plan office. You do not need a central register to know whether a colleague is at their desk — you glance across the room, notice whether their chair is occupied and their monitor is lit, and update your mental picture. If they step out for coffee, you eventually notice the empty chair and update. A presence system does exactly this at planetary scale: it maintains a rough, always-close-enough picture of who is currently “at their desk,” using cheap signals (heartbeats) instead of an expensive, always-perfectly-accurate ledger.

1.1 Why this system matters for an architect

Presence and typing indicators sit at the intersection of five hard sub-problems that architects are expected to reason about together:

  • Persistent-connection management at extreme scale — tens of millions of concurrent WebSockets are a different capacity-planning problem than any request-per-second system.
  • Failure detection over unreliable networks — a “silent” connection and a dead one look identical unless you probe.
  • Massive fan-out with skewed audience sizes — most audiences are tiny, but a few are enormous, and the same code path must serve both without pricing itself out of business.
  • Ephemeral, high-volume, low-value-per-event traffic — the cost-per-event math is unforgiving, so architectural choices matter far more than instance-type tuning.
  • Privacy and reciprocity constraints — who is allowed to see whose activity is itself a product decision, not just a technical filter.
02

Understanding the Problem Deeply

Before designing anything, it’s worth being precise about what “presence” and “typing indicator” actually mean as distinct problems, because they have very different consistency, durability, and latency requirements — a distinction that is easy to miss and that fundamentally shapes the architecture.

2.1 Presence vs. typing indicators: two different problems

DimensionOnline/Offline PresenceTyping Indicator
Lifespan of the signalCan persist for minutes to hours (a user stays “online” for their whole session)Extremely short-lived — typically expires within 3–10 seconds of the last keystroke
Durability needShould survive a brief server restart; “last seen” often persisted to diskZero durability need — if lost, nothing bad happens; the next keystroke re-sends it
Fan-out audienceEvery contact/friend who has this user in their contact list — can be very large (thousands) for public or business accountsOnly participants actively viewing that specific conversation right now — usually small (1 to a few hundred)
Consistency requirementEventually consistent is fine; a few seconds of staleness on “online” status is invisible to usersMust feel instantaneous (under ~100–300 ms) or it feels broken/laggy
Write frequencyChanges on connect/disconnect and periodic heartbeat — moderate frequencyChanges on every keystroke pause — extremely high frequency per active conversation

This distinction matters because it means the two features, despite looking similar in the UI, are best served by architecturally different data paths: presence benefits from being cached, batched, and eventually-consistent, while typing indicators are pure ephemeral pub/sub events that should never touch a durable database at all in the common case.

2.2 The core hard problem: distinguishing “disconnected” from “silent”

The fundamental difficulty in presence systems is that a dropped TCP/WebSocket connection and a client that is simply not sending anything right now look identical from the server’s point of view unless you actively probe for the difference. A mobile client can lose connectivity in a tunnel without either side receiving an explicit “goodbye” — the server has to infer offline status from the absence of expected signals (heartbeats), which is inherently a timing/threshold problem, not a binary fact. This is fundamentally different from most distributed-systems failure detection problems, where you can often afford to wait a long time before declaring a node dead; here, the whole point of the feature is to reflect reality quickly, so the grace period has to be short enough to feel responsive while still being long enough to tolerate ordinary transient network hiccups (a brief cellular handoff between towers, a few seconds of Wi-Fi-to-cellular transition) without generating false “offline” flickers.

💬
What an interviewer may ask

“How do you know a user is actually offline if their connection just silently disappeared without sending a close frame?” A strong answer discusses heartbeat-based liveness detection: clients send periodic pings (or the server pings clients) at a fixed interval, and if no heartbeat is received within a defined grace period (e.g., 1.5–2x the heartbeat interval), the server marks the connection dead and the user offline — accepting that there is always a small, bounded window of uncertainty rather than pretending perfect real-time truth is achievable over an unreliable network.

03

Requirements & Scale Assumptions

3.1 Functional requirements

  • Track each user’s online/offline/away status in real time, visible to their contacts or group members.
  • Support “last seen at [time]” for users who are currently offline.
  • Propagate a “typing…” indicator to other participants in a specific conversation within a low, near-instant latency, and automatically clear it after a short timeout of inactivity.
  • Support multiple simultaneous device sessions per user (phone, desktop, web) with correct aggregate presence (user is “online” if any device is connected).
  • Support both 1:1 conversations and large group conversations/channels (up to thousands of members) for both features.
  • Allow users to control presence visibility (privacy settings — e.g., “last seen” hidden from non-contacts).

3.2 Non-functional requirements

  • Scale: Hundreds of millions of registered users, tens to hundreds of millions of concurrently connected sessions at peak, billions of presence state changes and typing events per day.
  • Latency: Typing indicator delivery under ~200 ms end-to-end for a snappy feel; presence updates can tolerate a few seconds of propagation delay.
  • Consistency: Eventual consistency is fully acceptable for both features — this is one of the rare subsystems in a messaging platform where strict consistency is actively the wrong goal, since it would add unnecessary latency and cost for a UI signal users already intuitively understand as “approximate.”
  • Availability: The core messaging path (sending/receiving actual messages) must never depend on presence/typing subsystems — these are strictly best-effort, additive features, and their failure must never degrade core message delivery.
  • Cost efficiency: Because presence/typing events are extremely high volume but low value-per-event, the system must be designed for very low marginal cost per event — this is not a subsystem where you can afford to write every state change durably to a relational database.
100M+Concurrent connections at peak
<200 msTyping indicator end-to-end target
15–30 sTypical heartbeat interval
0 writesDurability required for typing events
04

High-Level Architecture

The architecture centers on persistent WebSocket (or similar bidirectional) connections held open between clients and a fleet of connection-gateway servers, an in-memory presence store, and a lightweight pub/sub fan-out layer — deliberately kept separate from the durable message-storage path used for actual chat messages.

flowchart TB subgraph CLIENT[“Client Layer”] C1[“Client Device Phone”] C2[“Client Device Desktop”] C3[“Client Device Web”] end subgraph EDGE[“Edge and Gateway Layer”] GW[“Connection Gateway Fleet stateful sticky sessions”] end subgraph APP[“Application Service Layer”] PM[“Presence Manager Service”] TM[“Typing Event Router”] HB[“Heartbeat Monitor TTL expiry”] NOTIFY[“Presence Fan-out Service”] end subgraph DATA[“Data and Messaging Layer”] PSTORE[“In-Memory Presence Store sharded Redis”] CONNREG[“Connection Registry user to gateway node mapping”] PUBSUB[“Pub Sub Fan-out Layer Redis Pub Sub or Kafka”] LASTSEEN[“Last-Seen Persistence async batched”] DB[“Durable Store Cassandra or DynamoDB”] end subgraph SUBS[“Subscribers”] RCPT[“Contacts and Group Members”] end C1 –> GW C2 –> GW C3 –> GW GW –> PM GW –> TM PM –> PSTORE PM –> HB TM –> PUBSUB PUBSUB –> GW PM –> CONNREG GW –> CONNREG PM –> LASTSEEN LASTSEEN –> DB PM –> NOTIFY NOTIFY –> RCPT RCPT –> GW
Figure 4.1 — High-level architecture. Ephemeral presence and typing traffic flows through in-memory stores and pub/sub only; the durable database is touched only asynchronously and only for “last seen” history.

4.1 Component-by-component breakdown

Edge

Connection Gateway Fleet

A horizontally-scaled fleet of stateful servers, each holding tens to hundreds of thousands of live WebSocket connections. Unlike typical stateless HTTP services, these servers are inherently stateful — each holds the actual live socket for a set of users — so routing and scaling strategy differ meaningfully from a normal microservice.

Routing

Connection Registry

A fast lookup service mapping “user ID / device ID → which gateway node currently holds their connection.” Any service that needs to push something to a specific user (a typing event, a presence update, an actual message) first consults this registry to know where to route it.

Core

Presence Manager Service

Owns the logic for what counts as “online” — tracks per-device connection state, runs the heartbeat-timeout logic, and computes the aggregate multi-device presence for a user.

Store

In-Memory Presence Store

A sharded, in-memory key-value store (typically Redis) holding current presence state per user with TTL-based auto-expiry — the single most important design choice in this system, discussed in depth below.

Liveness

Heartbeat Monitor

Periodically checks for connections that have not sent an expected heartbeat within the grace window and triggers offline transitions, typically driven by the store’s own TTL-expiry mechanism rather than an active scan loop.

Router

Typing Event Router

Receives raw “user X started/stopped typing in conversation Y” events from the gateway and routes them, purely in-memory, to the pub/sub fan-out layer — this path is intentionally kept as short and database-free as possible.

Fan-out

Pub/Sub Fan-out Layer

Delivers ephemeral events (typing signals, presence change notifications) to all currently-connected subscribers of a conversation or a user’s contact graph, without any durable storage step.

History

Last-Seen Persistence

A deliberately asynchronous, batched write path that periodically flushes “user went offline at time T” into a durable store, used only for the “last seen at…” UI feature when a user is offline — this is the only part of the system that touches durable storage, and even this is heavily batched to control write volume.

💬
What an interviewer may ask

“Why is the Presence Store separated from the durable Last-Seen store — wouldn’t one system be simpler?” The two have wildly different write profiles: the presence store absorbs a heartbeat-driven, extremely high-frequency, no-durability workload, while the last-seen store gets sparse, batched, durable writes only on the offline transition. Merging them would force the durable store to accept the heartbeat write volume — the exact anti-pattern this design is built to avoid.

05

Internal Working

5.1 Why gateway servers are stateful (and why that’s unusual)

Most modern backend services are deliberately designed to be stateless, so that any request can be routed to any instance and instances can be freely added, removed, or restarted without coordination. The connection gateway fleet in this system is a deliberate exception, and it’s worth understanding why that exception is necessary rather than a design flaw. A WebSocket connection is, by its nature, a long-lived, stateful binding between a specific client and a specific server process holding the open socket — there is no way to “load balance” an already-established connection’s traffic across multiple servers the way you can with independent HTTP requests. This means the gateway fleet has to be treated with different operational discipline than the rest of the platform: capacity planning is driven by concurrent connection count rather than requests-per-second, deploys require connection draining rather than simple instance replacement, and routing any message to a specific user requires first consulting the Connection Registry to find which specific node currently holds their socket.

5.2 How a client becomes “online”

  1. The client establishes a persistent WebSocket connection to a gateway node (chosen via a load balancer, often with some geographic/latency-aware routing).
  2. On successful connection and authentication, the gateway registers the (user ID, device ID) → gateway node mapping in the Connection Registry, and writes an “online” entry with a short TTL into the Presence Store.
  3. The client begins sending periodic heartbeat pings (commonly every 15–30 seconds) over the same connection; each heartbeat refreshes the TTL on the presence entry, effectively acting as a lease renewal.
  4. The Presence Manager computes aggregate status: if any of a user’s registered devices has a live “online” entry, the user’s externally-visible status is “online”; if all devices’ entries have expired or been explicitly removed, the user transitions to “offline” and a last-seen timestamp is recorded.

5.3 How “offline” is detected

There are two paths to offline, and a robust design needs both:

  • Graceful disconnect: The client sends an explicit close frame (app backgrounded cleanly, user logged out), and the gateway immediately clears the presence entry and notifies the fan-out layer — this is the fast, clean path.
  • Ungraceful disconnect (the hard case): The connection simply drops — network loss, app killed by the OS, device battery died — with no close frame. Here, the system relies entirely on the TTL expiring: since every presence entry has a TTL refreshed only by heartbeats, the absence of a heartbeat within the grace window causes the entry to expire automatically, and this expiry itself (using Redis keyspace notifications or a similar expiry-event mechanism) triggers the offline transition and fan-out, without any polling loop needed to “notice” the absence.
PresenceManager.java — heartbeat refresh as a TTL lease renewal
public void onHeartbeat(String userId, String deviceId) {
    String key = presenceKey(userId, deviceId);
    // Idempotent lease renewal - no read needed, just extend the TTL.
    presenceStore.setWithTtl(key, ONLINE_MARKER, HEARTBEAT_TTL);
}

public void onGracefulDisconnect(String userId, String deviceId) {
    presenceStore.delete(presenceKey(userId, deviceId));
    fanOut.publish(new PresenceChanged(userId, computeAggregate(userId)));
}

// Redis keyspace notification handler - fires when any device key expires.
public void onKeyExpired(String expiredKey) {
    UserDevice ud = parsePresenceKey(expiredKey);
    fanOut.publish(new PresenceChanged(ud.userId, computeAggregate(ud.userId)));
    lastSeenBuffer.enqueue(ud.userId, Instant.now()); // batched flush
}

5.4 How typing indicators flow

  1. As the user types, the client debounces keystrokes locally and sends a lightweight “typing” event to its gateway connection at most once every couple of seconds (never on every keystroke, to control volume) while actively typing, and a “stopped typing” event after a short pause (commonly 3–5 seconds of inactivity) or on message send.
  2. The gateway forwards this event to the Typing Event Router, tagged with the conversation ID.
  3. The router looks up which users are currently active participants viewing that conversation (not necessarily all group members — many clients only fan out typing indicators to members who have the conversation currently open) and publishes the event to the pub/sub layer keyed by conversation ID.
  4. The pub/sub layer delivers the event to the gateway nodes holding connections for each relevant recipient, which push it down each recipient’s WebSocket.
  5. On the receiving client, a local timer automatically clears the “typing…” UI after a few seconds even without an explicit “stopped typing” event — a crucial client-side safety net for the ungraceful-disconnect case, where the typer’s device drops without ever sending “stopped typing.”
💬
What an interviewer may ask

“Why not just write every typing event to a database and have clients poll or subscribe to changes?” This tests understanding of the ephemeral, disposable nature of the signal. The right answer: a typing indicator has a useful lifespan of a few seconds and zero value once stale — persisting it to durable storage adds write amplification and latency for a signal that should be treated as fire-and-forget, in-memory pub/sub, never touching a database. Any client-side safety-net timeout naturally cleans up the UI even if the “stopped” event is lost entirely, so durability buys nothing here.

06

Data Flow & Lifecycle

sequenceDiagram participant A as User A typing participant GWA as Gateway A connection participant ROUTER as Typing Event Router participant PUBSUB as Pub Sub Layer participant GWB as Gateway B connection participant B as User B viewing chat A->>GWA: keystroke debounced typing event GWA->>ROUTER: forward typing event conversation id ROUTER->>PUBSUB: publish event to conversation channel PUBSUB->>GWB: deliver to subscribed gateway node GWB->>B: push A is typing over WebSocket Note over A: pause greater than 4s no keystroke A->>GWA: stopped typing event or client timer GWA->>ROUTER: forward stop event ROUTER->>PUBSUB: publish stop event PUBSUB->>GWB: deliver stop event GWB->>B: clear typing UI
Figure 6.1 — End-to-end typing-indicator sequence. Note that no durable storage is touched at any point — the event is a pure in-memory fire-and-forget signal.

6.1 Presence lifecycle across a session

  • Connect: Presence entry created with short TTL; contacts/subscribers notified of “now online” via the fan-out layer (often with a small debounce to avoid flooding contacts during flaky reconnect loops).
  • Active session: TTL continuously refreshed by heartbeats; no further fan-out needed unless status changes (e.g., online → away after a period of client-detected inactivity).
  • Multi-device transitions: If a second device connects while the first is still active, aggregate presence remains “online” with no visible change; only when the last remaining device disconnects does the aggregate flip to “offline.”
  • Disconnect / TTL expiry: Presence entry removed; last-seen timestamp asynchronously persisted; “now offline” fan-out to subscribers.
  • Reconnect storms: After events like a regional network outage or app update rollout, millions of clients can reconnect within seconds — the system must handle this “thundering herd” of simultaneous connection and presence-write requests without falling over, discussed further in the scalability section.
stateDiagram-v2 [*] –> Connecting: WebSocket handshake Connecting –> Online: Auth OK, presence lease written Online –> Online: Heartbeat renews TTL Online –> Away: Client-detected inactivity Away –> Online: Foreground or activity Online –> Offline: Graceful close frame Online –> Offline: TTL expires no heartbeat Away –> Offline: TTL expires Offline –> Connecting: Client reconnects Offline –> [*]
Figure 6.2 — State diagram for a single device session. Aggregate user presence is the logical OR across all of a user’s device state machines.
07

Algorithms & Data Structures

7.1 CAP theorem applied to presence

Presence is one of the cleanest real-world illustrations of choosing availability and partition tolerance over strict consistency (AP over CP), and it’s worth being explicit about why. During a network partition between regions, a strictly consistent design would have to either block presence reads/writes until the partition healed, or risk serving contradictory answers — neither is acceptable for a feature users expect to always render something instantly. The AP choice means that during a partition, each region simply serves its own local view of presence (which may briefly disagree with another region’s view for cross-region contacts), and the two views reconcile naturally within one heartbeat cycle after the partition heals, with no manual conflict resolution required because the lease model has no conflicting writes to reconcile in the first place — only the most recent heartbeat matters, and stale entries expire on their own.

7.2 TTL-based lease expiry as the core primitive

The single most important algorithmic idea in this entire system is representing “online” as a lease with a TTL rather than as a persisted boolean flag that something has to remember to flip. This turns “detect that a user silently went offline” from an active polling/scanning problem into a passive expiry problem that the key-value store’s own engine handles efficiently — Redis, for example, uses a combination of lazy expiry (checked on access) and an active background sweep (a small random sample of keys checked periodically) to reclaim expired keys without a linear scan of the entire keyspace.

7.3 Consistent hashing for connection routing

The Connection Registry and Presence Store are sharded using consistent hashing over user ID, so that a given user’s presence data and connection mapping always land on a predictable, small set of nodes. This matters enormously at rebalancing time: when a node is added or removed from the fleet (routine scaling or a node failure), consistent hashing ensures only a small fraction of keys need to move, rather than a full reshuffle that would momentarily disrupt presence tracking for a huge fraction of users. A common refinement is to use virtual nodes (many logical shard positions per physical node) so that load distributes evenly even with a modest number of physical nodes, avoiding hotspots that plain consistent hashing can otherwise produce.

7.4 Debouncing and throttling for typing events

Naively sending a typing event on every keystroke would multiply write volume by the average word length, so clients apply local debouncing — collapsing a burst of keystrokes into at most one “typing” event per short interval (e.g., one event per 2 seconds while continuously typing), plus a single “stopped” event after a pause. This is a classic debounce/throttle pattern from event-driven systems, applied at the source to protect every downstream component from unnecessary load. Server-side, a secondary rate limiter acts as a backstop against clients that don’t debounce correctly (buggy clients, malicious clients, or older app versions), ensuring the fan-out layer’s load is bounded regardless of client behavior.

typingClient.ts — client-side debounce of keystrokes into bounded typing events
const TYPING_MIN_GAP_MS = 2000;
const STOPPED_AFTER_IDLE_MS = 4000;
let lastTypingSentAt = 0;
let idleTimer: number | null = null;

function onKeystroke(conversationId: string) {
  const now = Date.now();
  if (now - lastTypingSentAt > TYPING_MIN_GAP_MS) {
    ws.send({ type: 'typing', conversationId, state: 'start' });
    lastTypingSentAt = now;
  }
  if (idleTimer) clearTimeout(idleTimer);
  idleTimer = window.setTimeout(() => {
    ws.send({ type: 'typing', conversationId, state: 'stop' });
    lastTypingSentAt = 0;
  }, STOPPED_AFTER_IDLE_MS);
}

7.5 Fan-out: push vs. pull, and the “celebrity problem”

For 1:1 chats and small groups, a push-based fan-out (actively delivering the event to every subscriber’s gateway connection) is simple and cheap. For extremely large groups or public/celebrity accounts with millions of followers whose presence contacts want to see, naive push fan-out becomes a “thundering herd” write amplification problem — one status change fanning out to millions of connections instantly. The mitigation is a hybrid approach: for very large audiences, presence is served pull-style (a client queries current status when it renders the relevant UI, rather than being pushed every change), while push fan-out is reserved for small, bounded audiences like direct contacts and typing indicators in an actively-open conversation. A useful mental model borrowed from social-graph systems is the same one used for post fan-out on very large accounts: fan-out-on-write works well until audience size crosses a threshold, past which fan-out-on-read (pull) becomes cheaper, and a well-designed presence system dynamically picks the strategy per account based on observed audience size.

7.6 Bitmap/bitset representation for multi-device aggregation

For users with several registered devices, aggregate presence can be efficiently represented as a small bitset (one bit per active device session), where the aggregate “online” status is simply a bitwise OR across all bits — an extremely cheap operation that avoids needing to enumerate and check every device record on every presence query.

7.7 Approximate counting for aggregate metrics

Product and infrastructure dashboards often need aggregate figures like “concurrent online users right now” across the whole platform. Computing this exactly across every regional shard on every request is unnecessarily expensive for a number that only needs to be roughly right; approximate structures like HyperLogLog, or simply periodic sampled aggregation from each shard rolled up on a fixed interval, provide a good-enough answer at a tiny fraction of the cost of an exact global count.

💬
What an interviewer may ask

“How would you avoid a thundering herd when a whole region reconnects after a network blip, all trying to re-establish presence at once?” Expect discussion of jittered reconnect backoff on the client side (each client waits a randomized delay before reconnecting rather than all retrying at the exact same instant), connection-rate limiting at the load balancer/gateway layer, and gradual TTL-based presence expiry (rather than an instant mass “everyone offline” event) so the system absorbs the reconnect wave over a few seconds instead of a single spike.

08

Databases & Caching Strategy

StoreTechnology ChoiceWhy
Presence StoreSharded in-memory key-value store (Redis Cluster or similar) with TTLSub-millisecond reads/writes; native TTL expiry maps perfectly onto the lease model; presence data has no long-term durability requirement
Connection RegistryIn-memory key-value store, often co-located or replicated alongside the presence storeNeeds to answer “which gateway node holds this user’s connection” in microseconds on every routed event
Pub/Sub LayerRedis Pub/Sub for smaller deployments; Kafka or a dedicated pub/sub system (e.g., NATS) at very large scaleRedis Pub/Sub is simplest and fastest for pure fire-and-forget delivery; Kafka adds partitioned scalability and consumer-group flexibility when fan-out volume grows past a single Redis instance’s practical ceiling
Last-Seen Durable StoreWide-column store (Cassandra) or a managed NoSQL store (DynamoDB)Extremely high write volume of simple key-value “last seen at T” records, well suited to a store optimized for high-throughput, low-latency writes over a large flat keyspace rather than relational joins

8.1 Why not just use the primary relational database?

This is one of the most common design mistakes for engineers new to this problem: routing presence/typing writes through the same relational database used for user profiles and message metadata. At hundreds of millions of users with connect/disconnect/heartbeat events happening constantly, this write volume would overwhelm a relational database’s transactional write path almost immediately, and none of that data actually needs relational guarantees (foreign keys, joins, ACID transactions) — it needs raw throughput and automatic expiry, which is exactly what an in-memory store with TTL support is built for.

8.2 Caching strategy

Because presence status is read far more often than it changes (many clients render a contact list showing dozens of people’s status on every app open), a read-through cache in front of the presence store — or simply relying on the presence store itself being in-memory — keeps read latency low. For the pull-based “celebrity” presence path, an additional short-TTL cache (a few seconds) absorbs read bursts when a popular account’s profile is viewed by many users simultaneously, trading a few seconds of staleness for a massive reduction in backend query volume.

8.3 Batched writes for the last-seen path

Even the last-seen durable store, which is the only component that touches disk, does not receive individual writes per offline event. Instead, offline events are buffered in memory per node and flushed as a batched write every few seconds, dramatically reducing write amplification at the durable store while keeping the observable staleness of “last seen” timestamps well within the level users perceive as accurate.

💬
What an interviewer may ask

“What happens if the presence Redis cluster loses a node right now?” Good answer: because the data is short-lived and constantly refreshed, a replica is promoted and, within one heartbeat cycle, every affected client naturally re-populates its lease into the new primary — no complex reconciliation is needed. This self-healing property is one of the biggest reasons the lease model is the right primitive here.

09

APIs & Microservices Design

  • Connection/Gateway Protocol (WebSocket): Not a traditional REST API — a persistent bidirectional protocol carrying connection setup, heartbeats, typing events, and presence subscription/unsubscription messages as lightweight framed messages.
  • Presence Query API (used for the pull-based large-audience path): GET /v1/presence/{user_id} — returns current status and last-seen timestamp, served from cache wherever possible.
  • Typing Event API (sent over the existing WebSocket connection, not a separate REST call): a small framed message type, e.g. {type: "typing", conversation_id, state: "start"|"stop"}.
  • Presence Subscription API: Allows a client to subscribe to presence updates for a bounded set of contacts (its visible contact list), so the fan-out service only pushes updates to clients that have actually expressed interest, rather than broadcasting every user’s status globally.
  • Privacy Settings API: CRUD endpoints for a user’s presence-visibility preferences (e.g., “hide last seen from everyone except contacts”), consulted by the Presence Manager before including a user’s status in any fan-out or query response.

9.1 Sample API contracts

GET /v1/presence/{user_id} — pull-based presence lookup
GET /v1/presence/u_9f8a11
Response: {
  "userId": "u_9f8a11",
  "status": "online",           // "online" | "away" | "offline"
  "lastSeenAt": null,           // populated only when status = "offline"
  "visibilityScope": "contacts" // caller-scoped, respecting privacy settings
}
WebSocket frames — typing and heartbeat messages
// Client -> Server (typing)
{ "type": "typing", "conversationId": "c_88a...", "state": "start" }
{ "type": "typing", "conversationId": "c_88a...", "state": "stop"  }

// Client -> Server (heartbeat)
{ "type": "heartbeat", "ts": 1723371840 }

// Server -> Client (presence change fanned out to a subscriber)
{ "type": "presence", "userId": "u_9f8a11", "status": "offline",
  "lastSeenAt": "2026-08-11T18:22:14Z" }

The Connection Gateway fleet, Presence Manager, and Typing Event Router are deliberately kept as separate, independently-scalable services from the core message-storage and message-delivery services, so that a spike in typing/presence traffic (which is naturally much higher volume than actual message send volume) never contends for capacity with the guaranteed-delivery message path.

10

Design Patterns & Anti-Patterns

10.1 Patterns used

Pattern

Lease

Presence as a TTL-backed lease rather than a persisted flag — the core primitive of the whole design, and the reason the system self-heals within one heartbeat cycle after almost any failure.

Pattern

Publish/Subscribe

Both typing events and presence changes are pure pub/sub — publishers do not know or care who is listening, decoupling the event producers from the (varying, dynamic) set of interested clients.

Pattern

Sticky Sessions / Consistent Hashing

A user’s live connection is pinned to a specific gateway node for its duration, and consistent hashing keeps the routing table changes minimal during scaling events.

Pattern

Bulkhead Isolation

Presence/typing infrastructure runs on entirely separate services and often separate hardware capacity from the durable message-delivery path, so a presence-layer overload cannot cascade into failed message delivery.

Pattern

Graceful Degradation

Under extreme load, the system can drop typing-indicator delivery entirely (a cosmetic feature) while continuing to guarantee message delivery (a correctness-critical feature) — a deliberately designed priority ordering.

Pattern

Debounce & Throttle

Applied at the earliest possible point (the client) so that raw keystroke and heartbeat bursts are collapsed into bounded event rates before they ever touch the network, protecting every downstream component.

10.2 Anti-patterns to avoid

Anti-patternWhy it’s dangerous here
Treating presence as a strongly consistent, durably-stored factMassively over-engineers a feature that users already intuitively understand as approximate, at significant unnecessary infrastructure cost
Fanning out every typing keystroke without debouncingMultiplies event volume unnecessarily and provides no additional user value over a debounced signal
Global broadcast of presence changesNotifying literally everyone in the system of every status change, rather than only interested subscribers, wastes enormous bandwidth and compute at scale and does not survive contact with hundreds of millions of users
Coupling the presence/typing write path to the same database and service tier as core message storageRisks core messaging reliability for a best-effort cosmetic feature
Relying on an open TCP socket as proof of “online”Mobile networks routinely leave sockets half-open; without heartbeats the system will confidently show ghost users
📌
Trade-off: Push vs. Pull for presence at extreme fan-out

Push delivery (instantly notifying every subscriber of a status change) gives the snappiest UX but scales linearly with audience size per event, which becomes prohibitively expensive for celebrity/business accounts with huge follower counts. Pull delivery (clients query on demand, e.g., when rendering a profile) scales far better for huge audiences but introduces a small staleness window. The practical resolution most large platforms use is a hybrid: push for small, bounded audiences (direct contacts, active conversation participants), pull-with-short-cache for large, unbounded audiences.

11

Performance & Scalability

At hundreds of millions of users with tens to hundreds of millions of concurrent connections, three dimensions dominate the scaling conversation: the number of concurrently held connections per gateway node, the write throughput of the presence store under constant heartbeat traffic, and fan-out amplification for popular conversations and accounts.

  • Connection density per node: Modern event-driven network stacks (using epoll/io_uring-style asynchronous I/O rather than one OS thread per connection) allow a single gateway node to hold hundreds of thousands of concurrent idle WebSocket connections, since most connections are idle most of the time between heartbeats and events.
  • Horizontal gateway scaling: The gateway fleet scales horizontally by simply adding more nodes behind the load balancer; because connections are long-lived, scaling events (adding capacity) do not disrupt existing connections, but scaling down requires a careful, gradual connection-draining process to avoid mass-disconnecting users.
  • Sharding the presence store: Consistent-hash sharding across many in-memory store nodes spreads both the heartbeat write load and the read load for presence queries, with shard count sized to keep per-shard throughput well within the store’s comfortable operating range.
  • Heartbeat interval tuning: The heartbeat interval is a direct scalability lever — a longer interval (e.g., 30 s instead of 10 s) cuts presence-store write volume by 3x at the cost of a slower offline-detection window; this is a tunable trade-off platforms adjust based on observed load and acceptable staleness.
  • Regional deployment: Gateway fleets and presence stores are deployed regionally close to users to minimize connection latency, with cross-region replication only for the data that genuinely needs global visibility (e.g., a user’s aggregate online status, if they have contacts across regions), keeping the vast majority of heartbeat and typing traffic entirely regional. This regional-first approach also naturally aligns with data-residency requirements in some markets, since presence data for a region’s users never needs to leave that region except for the small aggregate status fact required by cross-region contacts.
graph LR A[“Client heartbeat”] –> B[“Regional Gateway”] B –> C[“Regional Presence Shard”] C –>|”only on status CHANGE not every heartbeat”| D[“Cross-Region Presence Sync”] D –> E[“Other Regions Presence Cache”]
Figure 11.1 — Regional locality: heartbeats stay entirely within a region; only actual status transitions cross the region boundary.

11.1 Capacity math walkthrough

Concrete numbers help calibrate intuition. Suppose the platform sustains $100$ million concurrent connections with a heartbeat every $20$ seconds. That is a global heartbeat rate of $100{,}000{,}000 / 20 = 5{,}000{,}000$ writes/second against the presence store — small per-shard if sharded across, say, $500$ nodes ($10{,}000$ writes/s per shard, well within a comfortable Redis operating range). Doubling the heartbeat interval to $40$ seconds cuts that to $2.5$ million/second globally, or $5{,}000$ per shard — a direct, linear cost lever the operator controls.

12

High Availability & Reliability

  • Gateway fleet redundancy: Many gateway nodes behind a load balancer with health checks; a node failure only affects the connections it was directly holding, which reconnect automatically (with jittered backoff) to a healthy node.
  • Presence store replication: Each shard is replicated (e.g., primary plus replicas) so a single node failure does not wipe out presence data for the users on that shard; given the short TTL nature of the data, even a brief data-loss window on failover is self-healing within one heartbeat interval as clients re-register.
  • Self-healing by design: This is one of the rare subsystems where the data model itself provides a natural recovery mechanism — because every presence fact is really a short-lived lease continuously refreshed by the client, the system recovers correct state automatically within one heartbeat cycle after almost any failure, without needing complex reconciliation logic.
  • Decoupling from core messaging reliability: As emphasized throughout, the architecture deliberately ensures that a full outage of the presence/typing subsystem degrades only cosmetic features, never message delivery — this is a conscious reliability-budget allocation decision, not an accident.
  • Circuit breaking on fan-out: If the pub/sub layer becomes overloaded, typing-event delivery is the first thing shed (clients simply stop seeing typing indicators temporarily) while presence updates, being lower frequency, continue to be served.
  • Replica promotion on shard failure: When a presence-store primary shard fails, a replica is promoted automatically (via the store’s built-in failover mechanism, such as Redis Sentinel or a cluster-mode equivalent), and because the data is short-lived and constantly refreshed, even a brief gap during failover is invisible to users — the next heartbeat from each affected client simply re-populates the new primary.
  • Load shedding under extreme spike: During an unusually large reconnect storm (e.g., following a major client app update pushed to hundreds of millions of devices simultaneously), the gateway layer can temporarily shed non-essential presence fan-out entirely while still accepting and authenticating connections, ensuring users can at least send and receive messages even if their online status momentarily lags.
💬
What an interviewer may ask

“What happens to in-flight typing indicators if a gateway node crashes?” A good answer: since typing state lives only in memory in the pub/sub layer and on the client (via the local safety-net timer), a crashed gateway simply drops whatever in-flight events it was routing — affected clients’ typing indicators either get delivered by a reconnect on a new gateway node moments later, or simply expire via the client-side timeout with zero lasting harm, since no durable state was ever at risk.

13

Security

  • Connection authentication: Every WebSocket connection is authenticated at handshake time (session token/JWT validated by the gateway) before any presence or typing data is accepted or served.
  • Authorization for presence visibility: The Presence Manager enforces each user’s privacy settings before including their status in any query response or fan-out — a user who has hidden “last seen” from non-contacts must never leak that data through a presence query.
  • Rate limiting on typing/presence events: Per-connection rate limits prevent a malicious or buggy client from flooding the system with excessive typing events, protecting the fan-out layer from abuse-driven overload.
  • Encrypted transport: All WebSocket connections use TLS; no presence or typing metadata is ever transmitted in the clear, since even “user X is typing” can be sensitive metadata (e.g., revealing someone is awake and active at an unusual hour).
  • Metadata minimization: Typing and presence events carry only the minimum necessary identifiers (user ID, conversation ID) and are never logged with full message content, keeping the security surface of this subsystem deliberately narrow.
  • Protection against presence enumeration attacks: Without safeguards, a malicious actor could probe the presence query API against a large list of phone numbers or user IDs to build a profile of who is currently active (a real privacy concern that has affected several messaging platforms historically). Mitigations include strict authorization checks (only contacts can query presence), aggressive rate limiting per querying account, and anomaly detection on query patterns that resemble bulk enumeration rather than normal contact-list rendering.
  • Denial-of-service resilience at the connection layer: Because the gateway fleet accepts a very large number of long-lived connections, it is a natural target for connection-exhaustion attacks; protections include per-IP connection limits, requiring authentication before a connection consumes significant server-side resources, and SYN-flood-resistant load balancing in front of the gateway tier.

13.1 Privacy controls as a first-class design concern

Presence is unusually privacy-sensitive compared to most metadata a messaging platform handles, because “is this person currently active” is itself a meaningful piece of information about someone’s life — whether they are awake, at work, avoiding a specific contact, or currently available. This is why mature platforms treat presence-visibility settings (who can see my online status, who can see my last-seen time, whether typing indicators are shown at all) as first-class, per-relationship privacy controls enforced consistently at the Presence Manager layer, rather than as an afterthought bolted onto the UI. A common design detail worth calling out: many platforms tie last-seen visibility reciprocally — if you hide your last-seen from others, you also lose the ability to see theirs — which is a product policy decision enforced at exactly the same authorization checkpoint as the technical privacy filtering.

💬
Common security mistake

Assuming that because presence data is “just a green dot,” it deserves less rigor than message content. In reality, aggregated presence patterns can reveal sleep schedules, locations, and behavioral routines — so the same authorization discipline applied to message reads must be applied to every presence query, and every enumeration attempt must be treated as a potential privacy breach in progress.

14

Monitoring, Logging & Metrics

14.1 Key metrics to track

MetricWhy it matters
Concurrent connections per gateway nodeCore capacity signal; drives auto-scaling of the gateway fleet
Connection churn rate (connects + disconnects per second)Detects reconnect storms early, before they overwhelm the presence store
Typing-event end-to-end latency (p50/p95/p99)Directly measures the user-visible responsiveness of the feature
Presence-store shard memory and eviction rateGuards against silent capacity exhaustion under a growing user base
Fan-out events published per secondCapacity signal for the pub/sub layer; correlated with conversation activity
Consumer lag on Kafka partitionsEarly warning that fan-out delivery is starting to fall behind real-time
Heartbeat interval vs. actual offline-detection timeValidates that TTL/grace-period tuning behaves as designed under real network conditions

14.2 Observability practices

  • Synthetic canary connections: Continuously-running test clients that measure real end-to-end typing-indicator latency from a known sender to a known receiver, catching regional or fleet-wide degradation before it’s reported by real users.
  • Per-region dashboards: Since connections and presence are largely regional, dashboards broken out by region make it easy to spot a single-region degradation rather than only seeing a diluted global average.
  • Heartbeat interval vs. actual offline-detection-time tracking: Monitoring the real observed delay between a client actually disconnecting and the system marking it offline, to validate that the TTL/grace-period tuning is behaving as designed under real-world network conditions.
  • Connection churn correlation with client releases: Tracking connection drop rates segmented by client app version and platform (iOS/Android/web) helps catch a buggy client release that reconnects too aggressively or fails to send heartbeats correctly, before it degrades the whole fleet’s load profile.
  • Shard hot-key detection: Monitoring for individual presence-store shards receiving disproportionate load (often caused by a small number of extremely popular accounts landing on the same shard) so that hot shards can be split or given dedicated capacity before they become a bottleneck.

14.3 Alerting philosophy

Alerts are tiered by user impact, not just raw metric threshold:

  • P1 (page immediately): mass connection drop rate above a fleet-wide threshold, presence-store shard down without replica promotion, cross-region sync completely stalled.
  • P2 (urgent): single-region typing latency regression above SLA, single shard’s hot-key ratio outside expected band.
  • P3 (informational): minor auto-scaling events, small latency drift within SLA.
15

Deployment & Cloud Architecture

  • Regional, multi-cluster deployment: Gateway fleets and presence-store shards are deployed per region close to users, minimizing connection round-trip latency and keeping the vast majority of traffic local.
  • Rolling, connection-draining deployments: Because gateway nodes hold long-lived stateful connections, deploys use a graceful drain process — new connections stop being routed to a node marked for replacement, existing connections are asked to reconnect gracefully (often with a brief delay to spread the reconnect load), and the old node is only terminated once drained.
  • Auto-scaling on connection count and CPU: Gateway fleets auto-scale primarily on live connection count and CPU utilization from the async I/O event loop, rather than traditional request-rate metrics, since a gateway node’s load is dominated by held-open connections, not discrete requests.
  • Cost optimization: Because this subsystem is extremely high volume but individually cheap per event, cost efficiency comes primarily from tuning heartbeat intervals, debounce windows, and choosing in-memory stores over durable ones wherever durability isn’t actually needed — architectural choices matter far more here than instance-type tuning.
  • Blast-radius containment: Deployment topology deliberately limits how much of the user base a single bad deploy or configuration change can affect at once — canary rollouts to a small percentage of gateway nodes in a single region first, with automated rollback triggers tied to connection-error-rate and latency regressions, before a change is promoted fleet-wide.

15.1 Infrastructure as code

The full topology — gateway pools, presence-store shards, pub/sub topics, IAM policies, and load balancer configuration — is declared in version-controlled infrastructure-as-code so any region can be re-created reproducibly. This matters both for disaster recovery and for expanding into a new region without hand-configuring dozens of moving parts under time pressure.

💬
What an interviewer may ask

“How would you deploy a change to the gateway without abruptly disconnecting millions of users?” A strong answer walks through connection draining (stop routing new connections to the target node, ask existing clients to reconnect with a jittered delay, only terminate the node once its connection count has drained to zero), combined with a canary rollout to a small percentage of nodes in a single region first before promoting fleet-wide.

16

Key Trade-offs Summary

DecisionOption AOption BRecommended Approach
Presence durabilityPersist every state change to a durable DBKeep presence purely in-memory with TTLIn-memory with TTL for live status; async batched writes only for “last seen” history
Offline detectionRely only on explicit disconnect signalHeartbeat + TTL-based inferenceBoth — explicit disconnect for the fast path, heartbeat TTL for the ungraceful-drop case
Large-audience fan-outPush to every subscriber on every changePull on demand with short cachePush for small/bounded audiences, pull-with-cache for large/unbounded audiences
Heartbeat intervalShort (fast detection, high load)Long (slow detection, low load)Tune per product needs; typically 15–30 s with a 1.5–2x grace period
Typing event granularitySend on every keystrokeDebounce to a fixed intervalDebounce — no meaningful UX loss, large load reduction
Cross-region syncSynchronous replication on every heartbeatAsynchronous, change-onlyAsynchronous, and only on actual status transitions — heartbeats stay regional
📌
The one-question tie-breaker

Whenever a design decision in this system is unclear, the tie-breaker question is: “does this change risk making our best-effort cosmetic feature able to degrade the guaranteed message-delivery path?” If the answer is even possibly yes, the design is wrong — the whole point of the isolation is that presence/typing can fail without messaging failing, and any coupling that erodes that property has to be rejected regardless of how much it simplifies the code.

17

Best Practices & Common Mistakes

17.1 Best practices

  • Model presence as a lease/TTL, never as a manually-managed boolean flag that something has to remember to flip on disconnect.
  • Keep the typing-indicator path entirely in-memory and database-free — durability adds cost and latency for zero user-visible benefit on a signal with a multi-second useful lifespan.
  • Debounce and throttle at the client, the earliest possible point, to protect every downstream system from unnecessary load.
  • Always implement a client-side safety-net timeout that clears a stale typing indicator even if the “stopped” event never arrives — never rely solely on the sender’s cooperation.
  • Architecturally isolate presence/typing infrastructure from the durable message-delivery path so a failure in one never threatens the other.
  • Use a hybrid push/pull fan-out strategy so a small number of very-large-audience accounts do not dictate the cost profile of the entire system.
  • Choose heartbeat and debounce intervals deliberately as explicit product/cost trade-offs, and revisit them as the user base grows rather than treating early defaults as permanent.
  • Prefer regional autonomy over global synchronous coordination — let each region serve its own local presence view and reconcile asynchronously, rather than paying cross-region latency on every heartbeat.

17.2 Common mistakes

  • Writing every heartbeat or typing keystroke to a relational database, which cannot sustain the write volume at hundreds of millions of users.
  • Assuming a TCP/WebSocket connection being open is sufficient proof of “online,” without accounting for silently dead connections on mobile networks.
  • Broadcasting presence changes globally instead of only to interested subscribers, wasting enormous bandwidth at scale.
  • Coupling gateway deploy/restart cycles to abrupt connection termination instead of graceful draining, causing visible disruption (mass “offline” flickers) during routine deployments.
  • Ignoring multi-device aggregation logic, leading to a user’s status flickering online/offline as they switch between phone and desktop rather than showing a stable aggregate status.
  • Skipping hysteresis on offline transitions, causing brief network hiccups to produce visible “offline…online…offline” flicker to every contact.
18

Real-World Industry Examples

Messaging

WhatsApp

Operating at billions of users with famously lean backend infrastructure, WhatsApp’s presence system has been publicly discussed as relying heavily on in-memory, ephemeral state rather than heavyweight durable storage, reflecting exactly the lease-based philosophy described in this tutorial — presence as a cheap, self-healing signal rather than a persisted fact.

Team Chat

Slack

Slack’s presence and typing-indicator systems are scoped per-workspace and per-channel rather than globally broadcast, illustrating the bounded-audience fan-out principle — a user’s presence is only actively pushed to people who share a workspace with them, not to the entire user base.

Communities

Discord

Discord serves extremely large guilds (servers) with hundreds of thousands of members, which pushed its engineering team toward smart, scoped presence delivery (only actively rendering and subscribing to presence for the subset of members visible in a client’s current view) rather than naive full-guild broadcast, directly reflecting the push-vs-pull, bounded-fan-out trade-off discussed above.

Social

Facebook Messenger

Messenger’s chat-head and contact-list presence features operate at a scale where the “celebrity problem” (public figures and business accounts with enormous follower/contact counts) is a real, named engineering challenge, motivating the hybrid push/pull approach for large audiences described in this design.

Channels

Telegram

Telegram supports very large public groups and channels (with membership counts far exceeding typical group chat sizes on other platforms), and its approach of scoping typing indicators to actively-open conversations rather than full-channel broadcast reflects the same bounded-audience principle discussed throughout this tutorial — full broadcast simply does not scale to channels with hundreds of thousands of members.

General Pattern

Shared Insight

Across all of these platforms, a consistent theme emerges: none of them treat presence and typing as an extension of the durable message-storage system. Every one of them, in their own way, has arrived at some version of the same architectural insight — ephemeral, high-frequency, low-value-per-event signals need their own lightweight, in-memory, self-healing infrastructure, kept deliberately separate from the guarantees and cost profile of reliable message delivery.

💬
What an interviewer may ask

“How would your design change if you had to support a public channel with 500,000 members, all of whom could be online at once?” A strong answer flips typing indicators to actively-open-conversation-only scope (not full-channel broadcast), moves presence to a pull-with-cache model for members outside a small “actively engaged” subset, and adds a per-channel rate limiter on how many concurrent typing events can be fanned out in any given second.

19

FAQ

Q1

Why is eventual consistency acceptable here when it would not be for, say, a bank balance?

Because presence and typing status are inherently transient, best-effort signals about a rapidly changing real-world state (a person’s current activity) — the user’s mental model already treats a “last seen 2 minutes ago” label as approximate, not as a guaranteed fact. There is no correctness invariant being violated by a few seconds of staleness, unlike a financial balance where staleness could cause a real, costly error.

Q2

How do you handle a user with the app open on five devices at once?

Each device maintains its own independent connection and presence lease; the Presence Manager computes an aggregate status as a logical OR across all of a user’s active device leases (represented efficiently as a small bitset), so the user appears “online” as long as at least one device has an active, unexpired lease, and only flips to “offline” once every device’s lease has expired.

Q3

Does a typing indicator ever need to reach someone who is not currently looking at the conversation?

Generally no — most platforms only fan out typing indicators to participants who currently have that specific conversation open in their client, since showing “X is typing” in a contact list or notification for a chat the recipient is not viewing provides little value relative to the added fan-out cost; this scoping decision significantly shrinks the typical typing-indicator audience compared to full-group broadcast.

Q4

What happens during a full regional outage — does everyone appear offline?

If an entire region’s gateway/presence infrastructure goes down, every user connected through that region will have their leases expire and appear offline to their contacts once the TTL grace period passes — this is a real and expected consequence of the lease model, and it is considered an acceptable, self-correcting failure mode (everyone reconnects and reappears online automatically once the region recovers) rather than a scenario requiring complex manual intervention.

Q5

How do you prevent a user’s status from flickering rapidly between online and offline on a flaky connection?

Flapping is a well-known nuisance in any heartbeat-based liveness system. The common mitigation is hysteresis: rather than immediately fanning out an “offline” notification the instant a lease expires, the Presence Manager waits a short additional grace window (a few seconds beyond the TTL) before declaring the change final, and if the client reconnects within that window, no offline/online flicker is ever shown to contacts at all. The same debounce is often applied symmetrically to rapid online-offline-online sequences.

Q6

Should typing indicators show in end-to-end encrypted conversations?

Yes, and this is a genuinely interesting design constraint — the typing signal itself (“user X is typing in conversation Y”) is metadata, not message content, so it does not need to be end-to-end encrypted the same way message bodies do, but platforms with strong metadata-privacy commitments still minimize what this metadata reveals (for instance, not exposing exactly what character count or content shape is being typed, only the boolean fact that typing is happening) and route it over the already-authenticated transport-layer-encrypted connection rather than a separate unencrypted channel.

Q7

How do group typing indicators avoid becoming unreadable when many people type at once?

Most clients cap the number of simultaneously displayed typing users (e.g., showing “Alice and Bob are typing…” and collapsing to “3 people are typing…” beyond a small threshold) rather than rendering an unbounded list — this is purely a client-side rendering decision, but it also usefully caps the amount of state the client needs to track and display, since the backend can simply deliver all individual events and let the client decide how to summarize them.

20

Summary & Key Takeaways

📌
The core mental model

A presence and typing-indicator system is, at its core, a lightweight, self-healing lease system layered on top of a stateful connection fleet and a pub/sub fan-out. Every design decision — TTL-backed leases, in-memory storage, debouncing at the client, hybrid push/pull for large audiences, hard isolation from the message-delivery path — traces back to one unavoidable fact: this is extremely high-volume, low-value-per-event traffic, so the architectural instincts that serve durable, consistent data are exactly the ones you must actively unlearn here.

20.1 Key takeaways to carry into an interview

  • Presence and typing indicators look like trivial UI features but are, underneath, hard distributed-systems problems involving massive persistent-connection fan-out, eventual consistency, and failure detection over unreliable networks.
  • The single most important design idea is treating presence as a TTL-backed lease rather than a durably-persisted flag — this makes the system self-healing by construction and avoids an entire category of “who resets this on failure” bugs.
  • Typing indicators should be treated as pure, ephemeral, in-memory pub/sub events with zero durability requirement, protected by both server-side debouncing and client-side safety-net timeouts.
  • Fan-out strategy must adapt to audience size: push for small, bounded audiences (contacts, active conversation participants) and pull-with-caching for large, unbounded audiences (celebrity/business accounts) to avoid the “thundering herd” cost of naive global broadcast.
  • This subsystem should be architecturally isolated from the durable, guaranteed-delivery message-storage path, so failures here degrade only a cosmetic feature and never threaten core messaging reliability.
  • At hundreds of millions of users, the scaling levers that matter most are connection density per gateway node, heartbeat interval tuning, and consistent-hash sharding of presence state — not exotic new algorithms, but disciplined application of well-understood distributed-systems fundamentals to an extremely high-volume, low-value-per-event workload.

20.2 The one idea to remember

If you take one architectural lesson from this guide into your next system design interview or your own production system, let it be this: separate the parts of your system that must be perfectly correct from the parts that only need to be roughly right. Messages must be delivered exactly once, in order, durably; presence and typing only need to feel accurate and disappear cleanly when wrong. Building both on the same durable, consistent substrate is the single most expensive mistake in this space — and building them on genuinely different substrates, with genuinely different reliability budgets, is what lets a single platform serve billions of users without the ephemeral traffic eating the entire cost of the durable one.