System Design Case Study

How does a chat system handle millions of ephemeral typing events per second?

🎯 Design a typing indicator system that delivers ephemeral events to conversation participants in sub-100ms without persisting anything to disk
Concepts Involved

How does a chat system handle millions of ephemeral 'typing...' events per second without persisting anything to disk, while ensuring sub-100ms delivery to all conversation participants?

Scope: Ephemeral typing indicator delivery only · not message send, not presence, not read receipts. The hard question: how do you fan-out millions of fire-and-forget events per second with zero disk I/O and sub-100ms latency✗
5M+
typing events / sec peak
ephemeral, never persisted
<100ms
delivery P99
must feel instantaneous
0 bytes
persisted to disk
pure in-memory routing
8s TTL
auto-expire timeout
no explicit "stopped" needed

Functional Requirements

What the system must do · ephemeral event delivery with aggressive throttling

Must Have (Core)

1. When a user types, all online participants in the conversation see "User is typing..."
2. Typing indicator auto-expires after 8 seconds of inactivity (no explicit stop event needed)
3. Events are throttled · max 1 typing event per 5 seconds per user per conversation
4. Zero persistence · typing events never touch disk, Kafka, or any durable store
5. Delivery latency <100ms P99 · must feel instantaneous to recipients
6. System handles 5M+ events/sec at peak without degradation

Out of Scope (this design)

Message delivery (separate Kafka-based pipeline · see Slack Real-Time Messaging)
Presence (online/offline status · different protocol)
Read receipts (persisted, different consistency model)
Offline delivery · typing events are meaningless if recipient is offline
Multi-device sync of typing state (only deliver to other users, not same user's devices)

Non-Functional Requirements

Quality constraints for ephemeral event delivery at massive scale

PropertyTargetWhy It Matters / Design Impact
LatencyP99 <100ms deliveryTyping must feel instant · any perceptible delay breaks the illusion of real-time presence.
Throughput5M+ events/sec peakAll active users typing simultaneously during peak hours. Must handle without queuing.
DurabilityZero (ephemeral, never persisted to disk)Typing indicators are transient signals · persisting them wastes I/O and adds no value.
Availability99.9% (best-effort)Brief outage is invisible · users won't notice a missed typing indicator.
DeliveryAt-most-onceMissed indicator is unnoticeable. Duplicate would cause UI flicker · worse than missing one.
BandwidthMinimal per event (~50 bytes payload)High event rate means even small payloads add up. Keep wire format tiny.

Scale Estimation

Deriving throughput requirements from concurrent user behavior

StepCalculationResult
110M concurrent users · 20% actively typing2M typing users
22M users · 1 event/5s (throttled)400K events/sec avg
3Peak factor 10·~4-5M events/sec
4Avg conversation size 5 people → 400K · 4 recipients1.6M deliveries/sec
5Redis Pub/Sub capacity: ~1M msgs/sec per nodeNeed 2-3 Redis nodes
6Memory for throttle state: 2M users · 32 bytes~64MB per gateway (trivial)

Architecture Overview

Direct gateway-to-gateway pub/sub · no Kafka, no database, pure in-memory routing for sub-100ms delivery

TYPING EVENT PATH · User A types in #general (no persistence) User A keydown event throttled: 1 per 5s Gateway-A server-side throttle dedup within 5s window In-Memory Pub/Sub Router channel → online user → gateway Redis Pub/Sub (no persistence) Gateway-B → ✔✔ Gateway-C → ➔ Gateway-D → ✔✔✔ Latency Breakdown Client → Gateway: ~2ms (WS frame) Redis Pub/Sub hop: ~1ms Gateway → Client: ~2ms (WS push) Total: ~5ms THROTTLE & TTL · Why No Kafka Client-Side Throttle Max 1 event per 5s per conversation Debounce: wait 300ms after last keystroke Reduces 60 keystrokes/min → 1 event/5s Server-Side Throttle (Gateway) In-memory map: {user_id+channel_id → last_ts} Drop if last event < 5s ago Prevents misbehaving clients from flooding TTL Auto-Expire (Client-Side) Client shows "typing..." for max 8s No "stopped typing" event needed Timer reset on each new typing event received WHY NOT KAFKA · Latency Math ✔ With Kafka: ~50-80ms added latency Produce (5ms) + Broker persist (10ms) + Consumer poll (30ms) + Fan-out (5ms) Total: ~50-80ms · too slow for ephemeral events, wastes disk I/O ✔ Direct Pub/Sub: ~5ms total Gateway → Redis Pub/Sub (1ms) → Target Gateway → WS push (2ms) No disk, no consumer lag, no partition assignment overhead

Key Design Decisions

Every decision optimizes for one thing: minimum latency with zero durability

DecisionChoiceWhyAlternative Rejected
TransportRedis Pub/Sub (gateway-to-gateway)Sub-ms fan-out, no persistence, fire-and-forget semantics match typing events perfectlyKafka: adds 50ms+ latency, persists to disk (wasteful for ephemeral data)
ThrottlingDual: client + serverClient reduces 60 keystrokes/min to 1 event/5s. Server enforces for misbehaving clients.Server-only: wastes bandwidth sending events that will be dropped
ExpiryTTL-based (8s client timer)No "stopped typing" event needed · reduces event volume by 50%. Timer resets on new event.Explicit stop event: doubles event volume, complex state machine
Delivery guaranteeAt-most-once (fire & forget)Missed typing indicator is invisible to user · no retry needed. Simplifies entire system.At-least-once: requires ACKs, retries, dedup · overkill for ephemeral UI hint
RoutingChannel-based pub/sub topicSubscribe to typing:{channel_id}. Only gateways with online members subscribe.User-level routing: too many subscriptions, O(members) lookups per event
Key insight: Typing indicators are the opposite of messages · they need speed over durability, at-most-once over at-least-once, and in-memory over persistent. Using the message delivery pipeline (Kafka) for typing events is an anti-pattern that adds 10· latency for zero benefit.

Resilience & Edge Cases

Typing indicators are lossy by design · the system optimizes for graceful degradation over correctness

#Failure / Edge CaseWhat BreaksHow It's Handled
1Gateway crashTyping events for users on that gateway are lostAcceptable · typing indicator disappears after 8s TTL. No recovery needed. Users reconnect to new gateway.
2Redis Pub/Sub node failureEvents not delivered to subscribed gatewaysRedis Sentinel promotes replica in ~5s. During failover, typing indicators simply don't show · invisible to users.
3User types then closes app"Typing..." shown for up to 8s after user left8s TTL auto-expires. Presence system detects disconnect and can optionally clear early.
450-person group chat, all typing50 typing events · 49 recipients = 2,450 deliveriesClient UI caps display at "3 people are typing..." · server can further throttle to top-3 most recent.
5Misbehaving client sends 100 events/secCould flood pub/sub channelServer-side throttle drops events within 5s window. Rate limit at Gateway: max 1 event/5s enforced.
Design philosophy: Typing indicators are a best-effort UI enhancement. The system is designed so that any failure mode results in the indicator simply not showing · never in a stuck "typing..." state (thanks to TTL) and never in message loss (typing events are separate from the message pipeline).

Tech Stack & Tradeoffs

Minimal stack · optimized for speed and simplicity over durability

ComponentTechnologyWhy ThisWhy Not X
Event TransportRedis Pub/SubSub-ms delivery, fire-and-forget, no persistence overhead. Channel-based subscription model.Kafka: persists to disk, adds 50ms+ latency. Overkill for ephemeral events.
Throttle StateIn-memory HashMap (per Gateway)O(1) lookup, no network hop. Map: {user+channel → last_event_ts}. Evicted on disconnect.Redis: adds 1ms per event for throttle check · unnecessary network hop.
Client TimersetTimeout (8s TTL)Auto-expires indicator without server involvement. Reset on each new event.Server-sent "stop" event: doubles traffic, complex state management.
WebSocket GatewaySame as message delivery (Go/Java)Reuses existing WS connections. Typing events are just another frame type on the same socket.Separate connection: wasteful, doubles connection count.
Subscription ManagementRedis Pub/Sub channels per conversationGateway subscribes to typing:{channel_id} when first user in that channel connects.Per-user subscriptions: O(members) subscriptions per channel, doesn't scale.
Cross-reference: The typing indicator system rides on the same WebSocket connection as the Slack Real-Time Messaging system. The Gateway terminates the WS, handles both message delivery (via Kafka) and typing events (via Redis Pub/Sub) on the same connection · different transport for different durability needs.

Interview Cheat Sheet

The 5 things an interviewer wants to hear about typing indicators at scale

? Ephemeral = No Kafka, No DB
Typing events are fire-and-forget. They never touch disk. Use Redis Pub/Sub for gateway-to-gateway delivery · sub-ms latency, zero persistence overhead. Kafka adds 50ms+ and wastes disk I/O on data that expires in 8 seconds.
? Dual Throttle: Client + Server
Client debounces keystrokes (300ms) then throttles to max 1 event per 5 seconds. Server enforces the same limit in-memory to protect against misbehaving clients. This reduces 60 keystrokes/min to 12 events/min per user per channel.
? TTL Auto-Expire (8s) · No Stop Event
Client shows "typing..." for 8 seconds max, then auto-hides. Each new typing event resets the timer. No explicit "stopped typing" event needed · cuts event volume in half and eliminates stuck-state bugs.
? At-Most-Once Delivery (Lossy by Design)
A missed typing indicator is invisible to the user · they just don't see "typing..." for a moment. No ACKs, no retries, no dedup. This simplifies the entire system and makes it resilient to any transient failure.
? Channel-Based Pub/Sub Subscription
Each Gateway subscribes to typing:{channel_id} for channels with connected users. When a typing event arrives, the Gateway pushes to all local users in that channel. Unsubscribe when last user in channel disconnects.
One-liner answer: "Typing indicators use Redis Pub/Sub for sub-ms gateway-to-gateway delivery with zero persistence, dual client+server throttling at 1 event per 5 seconds, 8-second TTL auto-expire on the client, at-most-once fire-and-forget semantics, and channel-based subscriptions so only relevant gateways receive events."