Loading content...
Beyond delivering messages, modern messaging systems provide rich awareness features: Is Bob online right now? When was he last active? Has he seen my message? These features create a sense of presence and connection that makes digital communication feel alive.
However, presence and receipts are architecturally challenging at scale. With 2 billion users, tracking who's online and propagating status updates creates massive real-time data flows. A naive implementation would flood the network with billions of presence updates per minute.
Moreover, these features sit at the intersection of utility and privacy. Users want to know when others have read their messages, yet many also want to hide their own online status. Designing these systems requires balancing technical efficiency with nuanced privacy controls.
This page explores the architecture of presence systems, the mechanics of delivery and read receipts, and the privacy considerations that shape their design.
You will understand presence system architecture at scale, delivery and read receipt protocols, privacy controls and their implementation complexity, and the trade-offs between real-time accuracy and system efficiency. These patterns apply to any application with online status indicators.
Presence refers to the real-time awareness of a user's availability state. In messaging, presence typically manifests as:
A user's presence transitions through several states:
1234567891011121314151617181920212223242526272829303132333435363738
PRESENCE STATES═══════════════ ┌─────────────┐│ OFFLINE │◄─────────────────────────────────────────┐│ │ │└──────┬──────┘ │ │ App opens, │ │ WebSocket connects │ Disconnect timeout ▼ │ (30-60 seconds)┌─────────────┐ ││ ONLINE │──────────────────────────────────────────┤│ │◄─────────────────┐ │└──────┬──────┘ │ │ │ User starts │ User stops │ │ typing │ typing │ ▼ │ │┌─────────────┐ │ ││ TYPING │──────────────────┘ ││ │ │└──────┬──────┘ │ │ │ │ Typing timeout (5-10s) │ └─────────────────────────────────────────────────┘ │ │ ▼ \ RECORDING \ (temporary state) \___________/ STATE TRANSITIONS:══════════════════OFFLINE → ONLINE: WebSocket connection establishedONLINE → OFFLINE: WebSocket disconnected (with timeout delay)ONLINE → TYPING: User begins typing in a chatTYPING → ONLINE: Typing stops (timeout or message sent)ONLINE → AWAY: (Optional) Inactive for N minutes but still connectedWhen a WebSocket disconnects, we don't immediately show 'offline.' Brief network glitches are common on mobile. A 30-60 second grace period prevents flickering between online/offline states. Only after sustained disconnection do we update status to offline and stamp 'last seen.'
At WhatsApp scale, presence creates enormous data volumes. Let's calculate the challenge:
12345678910111213141516171819202122232425262728
SCALE ANALYSIS══════════════ Users online simultaneously: ~300 millionAverage contacts per user: ~200Presence updates per user/hour: ~10 (online/offline/typing) BROADCAST MODEL (Naive):Each update broadcast to all contacts: 300M users × 10 updates/hour × 200 contacts = 600 billion notifications/hour = 167 million notifications/second = IMPOSSIBLE at this rate SUBSCRIPTION MODEL (Smart):Only send to users currently viewing contact info: User opens chat → subscribes to that contact's presence ~5% of contacts currently being viewed 300M × 10 × 10 active subscriptions = 30 billion/hour = 8 million notifications/second = Still high but manageable FURTHER OPTIMIZATION:• Batch presence updates (coalesce 5 seconds of changes)• Rate limit per-user presence queries• Cache presence with short TTL (30 seconds)• Lazy fetch: only query presence when chat opens Result: Presence can be reduced from 167M/sec to <1M/sec with smart design1234567891011121314151617181920212223242526272829303132333435363738394041
┌───────────────────────────────────────────────────────────────────────────┐│ PRESENCE ARCHITECTURE │└───────────────────────────────────────────────────────────────────────────┘ ┌──────────────────┐ ┌──────────────────┐ │ Alice's Device │ │ Bob's Device │ └────────┬─────────┘ └────────▲─────────┘ │ │ │ "I'm online" │ "Alice is online" │ "I'm typing to Bob" │ "Alice is typing" ▼ │ ┌────────────────────────────────────────────────────────────┐ │ CONNECTION SERVERS │ │ (Track actually connected users) │ └─────────────────────────┬──────────────────────────────────┘ │ │ Presence events stream ▼ ┌────────────────────────────────────────────────────────────┐ │ PRESENCE SERVICE │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Presence State Store (Redis) │ │ │ │ user:alice → {status: ONLINE, last_seen: null} │ │ │ │ user:bob → {status: OFFLINE, last_seen: 14:45} │ │ │ │ typing:alice:bob → {expires_at: now+10s} │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Subscription Manager │ │ │ │ bob_device → [subscribed_to: alice, carol, dave] │ │ │ │ On alice status change → notify bob_device │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Privacy Filter │ │ │ │ Check: Can Bob see Alice's presence? │ │ │ │ - Alice allows contacts? │ │ │ │ - Is Bob in Alice's contacts? │ │ │ │ - Has Alice blocked Bob? │ │ │ └──────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────┘Instead of broadcasting presence to all contacts, the system uses subscriptions:
When Bob opens a chat with Alice:
SUBSCRIBE(presence, alice_id)When Bob closes the chat:
UNSUBSCRIBE(presence, alice_id)This model limits presence traffic to active conversations only.
Rather than explicit unsubscribe calls (which may be missed on disconnect), subscriptions can have implicit TTL. 'Subscribe for 5 minutes, auto-renew while chat is open.' If device disconnects without unsubscribing, subscription expires automatically. This prevents zombie subscriptions for crashed clients.
The 'typing...' indicator creates real-time conversational awareness. Its implementation requires careful balance between responsiveness and efficiency.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
// Client-side typing detectionclass TypingIndicatorManager { private typingTimer: NodeJS.Timeout | null = null; private lastTypingSent: number = 0; private isTyping: boolean = false; private readonly DEBOUNCE_MS = 500; // Min time between sends private readonly TYPING_TIMEOUT_MS = 5000; // Auto-stop after inactivity constructor( private chatId: string, private ws: WebSocket ) {} // Called on every keystroke in the message input onKeyPress(): void { const now = Date.now(); // Start typing if not already if (!this.isTyping) { this.startTyping(); } // Debounce: don't send too frequently if (now - this.lastTypingSent > this.DEBOUNCE_MS) { this.sendTypingStatus(true); this.lastTypingSent = now; } // Reset auto-stop timer this.resetTypingTimer(); } private startTyping(): void { this.isTyping = true; this.sendTypingStatus(true); this.resetTypingTimer(); } private stopTyping(): void { if (this.isTyping) { this.isTyping = false; this.sendTypingStatus(false); if (this.typingTimer) { clearTimeout(this.typingTimer); } } } private resetTypingTimer(): void { if (this.typingTimer) { clearTimeout(this.typingTimer); } // Auto-stop after 5 seconds of no keystrokes this.typingTimer = setTimeout(() => { this.stopTyping(); }, this.TYPING_TIMEOUT_MS); } private sendTypingStatus(isTyping: boolean): void { this.ws.send(JSON.stringify({ type: 'TYPING_STATUS', chatId: this.chatId, isTyping: isTyping })); } // Called when message is sent (implicitly stops typing) onMessageSent(): void { this.stopTyping(); } // Called when leaving chat onChatClosed(): void { this.stopTyping(); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
TYPING INDICATOR FLOW═════════════════════ Alice types to Bob: 1. DETECTION (Alice's device) ────────────────────────── Keystroke detected → debounce check → If 500ms since last send: send TYPING_START Start 5s inactivity timer 2. TRANSMISSION ───────────── Alice → Server: { type: "TYPING", conversation_id: "alice_bob", is_typing: true } 3. SERVER PROCESSING ────────────────── - Store in Redis with 10s TTL: SET typing:alice:alice_bob 1 EX 10 - Privacy check: Can Bob see Alice's typing status? - If yes: Look up Bob's connection - Forward to Bob's device 4. DELIVERY TO BOB ──────────────── Server → Bob: { type: "TYPING", conversation_id: "alice_bob", user_id: "alice", is_typing: true } 5. DISPLAY (Bob's device) ─────────────────────── Show "Alice is typing..." in chat header Start 10s display timer (in case stop message is lost) 6. TYPING STOPS (Alice sends message or stops typing) ───────────────────────────────────────────────── Alice → Server: { type: "TYPING", is_typing: false } Server → Bob: { is_typing: false } Bob hides typing indicator EDGE CASES:- Alice disconnects mid-typing: Redis TTL expires, Bob's timer expires- Bob's device disconnects: Bob doesn't receive indicator (acceptable)- Group chat: Typing sent to all subscribed members (batched)In a 100-member group, if 10 people type simultaneously, that's 10 typing indicators × 99 recipients = 990 notifications. Solution: only show '3 people are typing...' (without names) for groups, with lower update frequency. Some apps limit typing indicators to small groups only.
Delivery receipts confirm that a message has reached the recipient's device. This is the transition from single checkmark (sent) to double checkmarks (delivered).
| State | Indicator | Trigger | Meaning |
|---|---|---|---|
| Sending | ⏱️ Clock | Message created locally | Awaiting server acceptance |
| Sent | ✓ Single | Server ACK received | Message durably stored on server |
| Delivered | ✓✓ Double | Recipient ACK received | Message received by recipient device |
| Read | ✓✓ Blue | Recipient viewed message | Recipient has seen the message |
| Failed | ❌ Error | Send failure | Message could not be sent |
123456789101112131415161718192021222324252627282930313233343536373839404142
DELIVERY RECEIPT FLOW═════════════════════ 1. MESSAGE RECEIVED BY SERVER ───────────────────────── Alice sends message msg_123 to Bob Server stores message Server → Alice: ACK(msg_123, status=SENT) Alice shows: ✓ 2. MESSAGE DELIVERED TO BOB ───────────────────────── Server pushes message to Bob's device Bob's device receives via WebSocket Bob's device stores locally Bob's device → Server: DELIVERY_RECEIPT(msg_123, timestamp) 3. RECEIPT FORWARDED TO SENDER ─────────────────────────── Server looks up sender (Alice) Server → Alice: RECEIPT(msg_123, status=DELIVERED, timestamp) Alice shows: ✓✓ 4. MULTI-DEVICE COMPLICATION ───────────────────────── If Bob has 3 devices: - Phone receives → DELIVERED - Tablet offline → not delivered yet - Desktop receives → already DELIVERED Status is DELIVERED when any device receives. (Alternatively: DELIVERED when all devices receive, but this rarely used) 5. OFFLINE RECIPIENT ────────────────── Bob is offline. Message stored in offline queue. Status remains SENT (single ✓). When Bob connects: - Sync delivers message - Delivery receipt sent - Alice's status updates to ✓✓Sending individual receipts for each message is inefficient. Receipts are typically batched:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
// Client-side receipt batchingclass DeliveryReceiptBatcher { private pendingReceipts: Map<string, DeliveryReceipt> = new Map(); private batchTimer: NodeJS.Timeout | null = null; private readonly BATCH_INTERVAL_MS = 500; // Send batch every 500ms private readonly MAX_BATCH_SIZE = 50; // Or when batch reaches 50 // Called when a message is received and stored queueReceipt(messageId: string, senderId: string, timestamp: number): void { this.pendingReceipts.set(messageId, { messageId, senderId, deliveredAt: timestamp }); // Start timer if not running if (!this.batchTimer) { this.batchTimer = setTimeout(() => this.flush(), this.BATCH_INTERVAL_MS); } // Flush immediately if batch is full if (this.pendingReceipts.size >= this.MAX_BATCH_SIZE) { this.flush(); } } private flush(): void { if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; } if (this.pendingReceipts.size === 0) return; // Group by sender for efficient routing const receiptsBySender = groupBy( Array.from(this.pendingReceipts.values()), r => r.senderId ); // Send one batch per sender for (const [senderId, receipts] of Object.entries(receiptsBySender)) { this.sendReceiptBatch(senderId, receipts); } this.pendingReceipts.clear(); } private sendReceiptBatch(senderId: string, receipts: DeliveryReceipt[]): void { this.ws.send(JSON.stringify({ type: 'DELIVERY_RECEIPTS', senderId, receipts: receipts.map(r => ({ messageId: r.messageId, timestamp: r.deliveredAt })) })); }} // Result: 50 messages received → 1 batch receipt sent// Instead of: 50 individual receipt messagesFor even more efficiency, use cumulative receipts: 'All messages up to sequence 47 are delivered.' This requires messages to be sequentially ordered per conversation, but reduces receipt traffic to O(1) per batch regardless of message count.
Read receipts indicate that the recipient has actually viewed a message, not just that their device received it. This transitions double gray checkmarks to blue.
What constitutes 'reading' a message? This is a design decision with multiple interpretations:
| Approach | Trigger | Accuracy | Privacy Impact |
|---|---|---|---|
| Chat opened | User opens the conversation | Low (may not scroll to new messages) | Lower (less granular) |
| Message visible | Message scrolls into viewport | Medium (message on screen) | Medium |
| Message visible for N seconds | Message visible for 2+ seconds | Higher (user probably saw it) | Medium |
| Explicit action | User taps or interacts with message | Highest (definitely engaged) | Low (optional action) |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
// Detecting when messages are 'read'class MessageReadTracker { private visibleMessages: Map<string, number> = new Map(); // messageId → firstSeenTime private readonly READ_THRESHOLD_MS = 1000; // Must be visible for 1 second private checkInterval: NodeJS.Timeout; constructor( private conversationId: string, private onMessagesRead: (messageIds: string[]) => void ) { // Check visibility every 500ms this.checkInterval = setInterval(() => this.checkVisibility(), 500); } // Called by intersection observer when messages enter/exit viewport onMessageVisible(messageId: string, isVisible: boolean): void { if (isVisible && !this.visibleMessages.has(messageId)) { // Message just became visible this.visibleMessages.set(messageId, Date.now()); } else if (!isVisible) { // Message left viewport before threshold this.visibleMessages.delete(messageId); } } private checkVisibility(): void { const now = Date.now(); const readMessages: string[] = []; for (const [messageId, firstSeenTime] of this.visibleMessages) { if (now - firstSeenTime >= this.READ_THRESHOLD_MS) { readMessages.push(messageId); this.visibleMessages.delete(messageId); } } if (readMessages.length > 0) { this.onMessagesRead(readMessages); } } // Example using Intersection Observer API setupIntersectionObserver(messageElements: HTMLElement[]): void { const observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { const messageId = entry.target.dataset.messageId!; this.onMessageVisible(messageId, entry.isIntersecting); }); }, { threshold: 0.8 } // 80% of message must be visible ); messageElements.forEach(el => observer.observe(el)); }}Group read receipts are significantly more complex:
Question: When do we consider a group message 'read'?
Options:
WhatsApp's implementation:
This requires tracking N read receipts per message (one per member), which grows as O(messages × members).
A 1000-member group with 1000 messages requires storing up to 1,000,000 read receipts for that conversation alone. At scale, this creates significant storage overhead. Many systems aggregate or limit detailed read tracking for large groups.
Presence and receipts reveal information about user behavior. Privacy-conscious design gives users control over what they share—but this control creates architectural complexity.
| Setting | Options | Implementation |
|---|---|---|
| Last Seen | Everyone / My Contacts / Nobody | Filter last_seen timestamp in presence response |
| Online Status | Everyone / Same as Last Seen | Filter online indicator in presence response |
| Read Receipts | On / Off | Skip sending read receipts server-side |
| Typing Indicator | On / Off (rare) | Skip sending typing status |
| Profile Photo visibility | Everyone / My Contacts / Nobody | Filter profile photo URL in user info |
WhatsApp enforces reciprocity for read receipts: if you turn off read receipts, you also can't see others' read receipts.
This prevents asymmetric advantage where someone hides their reads while tracking everyone else's.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
interface PresencePrivacySettings { lastSeen: 'everyone' | 'contacts' | 'nobody'; online: 'everyone' | 'contacts' | 'nobody'; readReceipts: boolean; profilePhoto: 'everyone' | 'contacts' | 'nobody';} class PresencePrivacyFilter { private contactsCache: Set<string>; /** * Determine if requester can see target's presence */ async canSeePresence( requesterId: string, targetId: string, presenceType: 'lastSeen' | 'online' | 'typing' ): Promise<boolean> { const targetSettings = await this.getPrivacySettings(targetId); const isBlocked = await this.isBlocked(targetId, requesterId); if (isBlocked) { return false; // Blocked users see nothing } const setting = this.getSettingForType(targetSettings, presenceType); switch (setting) { case 'everyone': return true; case 'contacts': return await this.isContact(targetId, requesterId); case 'nobody': return false; default: return false; // Safe default } } /** * Determine if we should send read receipt for this message */ async shouldSendReadReceipt( readerId: string, messageAuthorId: string ): Promise<boolean> { const readerSettings = await this.getPrivacySettings(readerId); // If reader has disabled read receipts, don't send if (!readerSettings.readReceipts) { return false; } return true; } /** * Filter presence response based on privacy settings */ async filterPresenceResponse( requesterId: string, targetId: string, presence: PresenceInfo ): Promise<PartialPresenceInfo> { const canSeeOnline = await this.canSeePresence(requesterId, targetId, 'online'); const canSeeLastSeen = await this.canSeePresence(requesterId, targetId, 'lastSeen'); return { userId: targetId, isOnline: canSeeOnline ? presence.isOnline : null, lastSeen: canSeeLastSeen ? presence.lastSeen : null, // Typing indicator follows online visibility isTyping: canSeeOnline ? presence.isTyping : null }; }}Group chats complicate privacy. Even if you disable read receipts, group members see when the message turns blue (everyone has read). Privacy settings typically don't apply to groups—or groups only show aggregate counts, not individual reads. The social contract in groups differs from 1:1 chats.
'Last seen at 3:45 PM' tells users when someone was last active. This seemingly simple feature has nuanced implementation considerations.
123456789101112131415161718192021222324252627282930
LAST SEEN UPDATE TRIGGERS═════════════════════════ Option 1: On disconnect └── Set last_seen = disconnect_timestamp when WebSocket closes └── Simple, but doesn't reflect actual activity Option 2: On any activity └── Update timestamp on message send, message receive, chat open... └── More accurate, but very high update frequency Option 3: Periodic heartbeat-based (WhatsApp's approach) └── Update last_seen with each heartbeat (~30 seconds) └── Reasonable accuracy, bounded update frequency Option 4: On transition to offline └── Only set when transitioning from ONLINE to OFFLINE └── Prevents constant updates while user is continuously active RECOMMENDED: Option 4 with debounce ───────────────────────────────── 1. User connects: status = ONLINE, last_seen = null 2. User is active: heartbeats keep ONLINE status 3. User disconnects: start 60-second grace period 4. If reconnects within 60s: cancel offline transition 5. After 60s still disconnected: - status = OFFLINE - last_seen = gracePeriodStartTime (when user actually disconnected) This gives accurate 'last active' time without constant database writes.Last seen timestamps use relative and contextual formatting for better UX:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
function formatLastSeen(lastSeenTimestamp: number): string { const now = Date.now(); const diff = now - lastSeenTimestamp; const diffMinutes = Math.floor(diff / 60000); const diffHours = Math.floor(diff / 3600000); const diffDays = Math.floor(diff / 86400000); // Currently online if (lastSeenTimestamp === null) { return 'online'; } // Within last minute if (diffMinutes < 1) { return 'last seen just now'; } // Within last hour if (diffMinutes < 60) { return `last seen ${diffMinutes} minute${diffMinutes > 1 ? 's' : ''} ago`; } // Today if (isToday(lastSeenTimestamp)) { return `last seen today at ${formatTime(lastSeenTimestamp)}`; } // Yesterday if (isYesterday(lastSeenTimestamp)) { return `last seen yesterday at ${formatTime(lastSeenTimestamp)}`; } // Within last week if (diffDays < 7) { return `last seen ${getDayName(lastSeenTimestamp)} at ${formatTime(lastSeenTimestamp)}`; } // Older return `last seen ${formatDate(lastSeenTimestamp)}`;} // Examples:// "online"// "last seen just now"// "last seen 5 minutes ago"// "last seen today at 3:45 PM"// "last seen yesterday at 10:30 AM"// "last seen Monday at 2:15 PM"// "last seen Dec 15, 2024"Some apps deliberately reduce precision. Instead of 'last seen at 3:47 PM,' showing 'last seen around 3:45 PM' or 'last seen this afternoon.' This subtle fuzzing provides some privacy while maintaining utility. It also reduces the stalking potential of precise timestamps.
Presence and receipts must propagate quickly for features to feel 'real-time.' This requires efficient pub/sub architecture.
12345678910111213141516171819202122232425262728293031323334353637
PRESENCE SYNC ARCHITECTURE══════════════════════════ ┌─────────────────────────────────────────────────────┐ │ REDIS PUB/SUB │ │ │ │ Channel: presence:alice │ │ Subscribers: [conn_server_1, conn_server_5, ...] │ │ │ │ On presence change for alice: │ │ PUBLISH presence:alice {online: true} │ │ │ └─────────────────────────────────────────────────────┘ │ ┌─────────────────────┼─────────────────────┐ │ │ │ ▼ ▼ ▼ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ Conn Server 1 │ │ Conn Server 2 │ │ Conn Server 5 │ │ │ │ │ │ │ │ Bob's conn │ │ Carol's conn │ │ Dave's conn │ │ (watching │ │ (watching │ │ (watching │ │ alice) │ │ alice) │ │ alice) │ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │ │ │ ▼ ▼ ▼ Bob's Device Carol's Device Dave's Device OPTIMIZATION: Batched Fan-Out─────────────────────────────Instead of publishing each presence change: 1. Accumulate changes for 100ms2. Batch-publish: { alice: online, bob: typing, carol: offline }3. Each connection server filters relevant updates for its clients4. Reduces Redis pub/sub message count significantlyWhen a user has multiple devices, presence and receipts must sync across all of them:
Scenario: Bob has phone and desktop. Alice sends a message.
Implementation: User's devices are connected via shared state. When one device sends a read receipt, the server also notifies other devices of the state change.
1234567891011121314151617181920212223242526272829303132333435363738394041
// When a device marks messages as readasync function handleReadReceipts( userId: string, sourceDeviceId: string, messageIds: string[]): Promise<void> { // 1. Store read status in database await db.messageReceipts.upsertMany( messageIds.map(msgId => ({ messageId: msgId, userId: userId, readAt: Date.now() })) ); // 2. Send read receipts to message authors const messages = await db.messages.findMany({ where: { id: { in: messageIds } } }); for (const msg of messages) { await sendReceiptToAuthor(msg.authorId, { type: 'READ', messageId: msg.id, readBy: userId, timestamp: Date.now() }); } // 3. Sync to user's other devices const otherDevices = await getUserDevices(userId); const otherDeviceIds = otherDevices .filter(d => d.id !== sourceDeviceId) .map(d => d.id); await broadcastToDevices(otherDeviceIds, { type: 'LOCAL_READ_SYNC', messageIds: messageIds, // Other devices update their local UI to show these as read });}Multi-device receipt sync is eventually consistent. Device A marks read, device B learns within seconds. Occasional discrepancy (device A shows blue, device B briefly shows gray) is acceptable. Strict consistency would require complex coordination protocols, adding latency.
At billion-user scale, even small inefficiencies in presence/receipt systems compound into massive overhead. Key optimizations include:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Presence updates/sec | 167 million | < 1 million | 99%+ reduction |
| Receipt messages/sec | 5 million | 100 thousand | 98% reduction |
| Redis operations/sec | 500 million | 10 million | 98% reduction |
| Presence latency (P99) | 500ms | 100ms | 5x faster |
| Battery impact | Noticeable | Negligible | Significant |
Under extreme load, presence can be the first feature to degrade. Users tolerate slightly stale 'last seen' or delayed read receipts better than delayed messages. Implement circuit breakers that disable real-time presence during overload, falling back to on-demand queries.
Presence and delivery receipts transform messaging from a mailbox into a live connection. These features require sophisticated architecture balancing real-time responsiveness with scale and privacy.
Module Complete!
You have now explored the complete architecture of a WhatsApp-like messaging system: from requirements analysis through message delivery, real-time infrastructure, end-to-end encryption, media handling, and presence/receipts. These patterns combine to create the seamless, secure, instant communication billions rely on daily.
You now understand presence and delivery receipt systems comprehensively. These awareness features—online status, last seen, typing indicators, and read receipts—are found in virtually every modern communication app. The patterns of subscription-based updates, batching, and privacy filtering apply broadly to real-time collaborative applications.