Best Practices

AI Rules for Chat and Messaging

AI polls the server every second for new messages. Rules for WebSocket connections, message ordering, read receipts, typing indicators, offline message queuing, and conversation threading.

8 min read·March 14, 2025

HTTP polling every second — 86,400 requests per user per day, 99% returning empty results

WebSocket delivery, sequence ordering, read receipts, typing indicators, offline sync, threading

AI Builds Chat with HTTP Polling

AI generates chat with: HTTP polling every second (GET /api/messages?since=timestamp — 1 request per second per user, 86,400 requests per user per day), no message ordering guarantee (messages arrive out of order on slow connections), no read receipts (sender does not know if the message was seen), no typing indicators (no feedback while the other person types), no offline handling (messages sent while offline are lost), and no threading (flat list of messages, no replies or threads). The chat works for a demo but fails under any real usage pattern.

Modern chat messaging is: WebSocket-delivered (persistent connection, server pushes messages instantly, zero polling), ordered (sequence numbers guarantee correct display order), receipt-tracked (sent, delivered, read statuses per message), typing-indicated (debounced typing events show who is actively composing), offline-resilient (messages queued locally, synced on reconnect), and threaded (reply-to references, collapsible threads, channel organization). AI generates none of these.

These rules cover: WebSocket real-time delivery, message ordering with sequence numbers, read receipts, typing indicators, offline queuing with sync, and conversation threading.

Rule 1: WebSocket Real-Time Delivery

The rule: 'Use WebSocket connections for real-time message delivery. The client opens a persistent connection on app load; the server pushes messages through it instantly. No polling, no delay, no wasted requests. Library: Socket.IO (fallback to long-polling for older clients), or native WebSocket with a reconnection wrapper. For serverless: use a managed WebSocket service (Ably, Pusher, AWS API Gateway WebSocket) since serverless functions cannot hold persistent connections.'

For connection lifecycle: 'On connect: authenticate the WebSocket (send JWT on connection, verify server-side), join conversation rooms (Socket.IO rooms or channel subscriptions), and sync missed messages (fetch messages since last seen timestamp). On disconnect: detect within 30 seconds (heartbeat/ping-pong), attempt automatic reconnect with exponential backoff (1s, 2s, 4s, 8s, max 30s), and on reconnect sync missed messages. The user should never manually reconnect — it is automatic and invisible.'

AI generates: setInterval(() => fetch('/api/messages?since=' + lastTimestamp), 1000) — one HTTP request per second. 1000 active users: 1000 requests per second, 99% of which return empty results (no new messages). WebSocket: the server pushes only when a message exists. 1000 users with 10 messages per minute: 10 pushes per minute instead of 60,000 polls per minute. 6000x fewer operations for the same functionality.

  • WebSocket: persistent connection, server pushes instantly, zero polling overhead
  • Socket.IO: WebSocket with long-polling fallback, rooms, auto-reconnect
  • Serverless: Ably, Pusher, or AWS API Gateway WebSocket for managed connections
  • Auto-reconnect with exponential backoff: 1s, 2s, 4s, 8s, max 30s
  • Sync on reconnect: fetch messages since last seen timestamp, no gaps
💡 6000x Fewer Operations

HTTP polling: 1000 users x 1 request/second = 60,000 requests per minute, 99% empty. WebSocket: server pushes only when a message exists. 1000 users with 10 messages/minute = 10 pushes/minute. Same functionality, 6000x fewer operations.

Rule 2: Message Ordering with Sequence Numbers

The rule: 'Assign a monotonically increasing sequence number to each message within a conversation. The server assigns the sequence on receipt (not the client — client clocks are unreliable). Display messages sorted by sequence number, not by timestamp. If message 5 arrives before message 4 (network reordering), the client holds message 5 and displays it after message 4 arrives. Gaps in the sequence trigger a fetch for missing messages.'

For conflict resolution: 'Two users send a message at the exact same millisecond. With timestamps: the display order depends on clock synchronization (undefined). With sequence numbers: the server assigns sequential numbers — one gets 42, the other gets 43. The order is deterministic and consistent for all clients. For multi-server environments: use a centralized sequence generator (Redis INCR, Postgres sequence) or a distributed ID that preserves ordering (ULID, Snowflake ID).'

AI generates: messages sorted by createdAt timestamp. User A (clock 2 seconds fast) sends a message, User B responds. Display: B response appears before A message (B timestamp is earlier due to clock skew). With sequence numbers: A message gets sequence 42, B response gets sequence 43. Always displayed in send order, regardless of client clock accuracy.

⚠️ Clock Skew Breaks Order

User A (clock 2s fast) sends a message, User B responds. Sorted by timestamp: B appears before A. Sequence numbers: server assigns 42 to A, 43 to B. Always correct order regardless of client clock accuracy. Deterministic, consistent for all viewers.

Rule 3: Read Receipts and Typing Indicators

The rule: 'Track message status: sent (message saved to database), delivered (pushed to recipient WebSocket), read (recipient scrolled the message into view). Update the sender UI with status icons: single check (sent), double check (delivered), blue double check (read). Implementation: on message save → emit sent status. On WebSocket push to recipient → emit delivered status. On recipient viewport intersection (IntersectionObserver) → emit read status back through WebSocket.'

For typing indicators: 'When the user starts typing: emit a typing event with debounce (emit on first keystroke, then not again for 3 seconds). When the user stops typing: emit a stop-typing event after 3 seconds of inactivity. Display: "Alice is typing..." with an animated dots indicator. Multiple users typing: "Alice and Bob are typing...". The typing indicator disappears automatically after 5 seconds without a refresh (timeout safety — in case the stop event is lost).'

AI generates: no read receipts (sender does not know if the message was seen — they send it again, creating duplicates) and no typing indicators (two people type simultaneously, both send, awkward cross-talk). Read receipts reduce duplicate messages. Typing indicators reduce message collisions. Both are expected UX in any modern chat — their absence feels broken, not minimal.

Rule 4: Offline Message Queuing and Sync

The rule: 'When the user is offline (WebSocket disconnected, no network), queue messages locally. On reconnect: (1) send all queued messages to the server in order, (2) fetch all messages received while offline (since last seen sequence number), (3) merge local and remote messages by sequence number, (4) resolve conflicts (message sent offline may conflict with a server state change). Local queue: IndexedDB or localStorage. Messages appear instantly in the UI with a pending status (clock icon), then update to sent (checkmark) on server confirmation.'

For the sync protocol: 'On reconnect, the client sends: { lastSeenSequence: 142, pendingMessages: [{ localId: "abc", content: "hello", sentAt: "..." }] }. The server: assigns sequence numbers to pending messages, returns all messages after sequence 142, and the client merges everything. The client UI: pending messages transition from clock icon to checkmark. Missed messages appear in the correct position (by sequence number). The conversation is complete and correctly ordered after sync.'

AI generates: messages lost when offline. User types a response on the subway (no signal), hits send, navigates away. The message disappears. With offline queuing: the message is saved locally, shown with a pending indicator, and automatically sent when the connection returns. The user never knows they were offline — the message just takes a few extra seconds to confirm.

  • Local queue in IndexedDB: messages saved immediately, sent on reconnect
  • Pending status (clock icon) → sent (checkmark) on server confirmation
  • Sync on reconnect: send pending + fetch missed since lastSeenSequence
  • Merge by sequence number: local and remote messages in correct order
  • User never loses a message: offline, slow connection, or reconnecting — all handled
ℹ️ User Never Loses a Message

Offline on the subway: message saved to IndexedDB, shown with a clock icon. Connection returns: message auto-sent, icon changes to checkmark. Missed messages fetched by sequence number. The user never knows they were offline — the message just confirms a few seconds late.

Rule 5: Conversation Threading Models

The rule: 'Support threaded conversations: each message can have a replyTo field referencing the parent message ID. Display options: inline replies (indented under the parent, like iMessage), sidebar threads (click a message to open its thread in a side panel, like Slack), or flat with reply preview (show the quoted parent above the reply, like WhatsApp). The threading model depends on your use case: inline for simple chat, sidebar for team collaboration, flat-with-preview for mobile.'

For channel organization: 'Group conversations into channels (team chat) or direct messages (1:1 or small group). Channel model: { id, name, type: "channel" | "dm", members: userId[], lastMessageAt, unreadCount }. Each channel has its own message sequence. Unread count: tracked per user per channel — increment on new message, reset when the user opens the channel. The channel list sorts by lastMessageAt (most recent activity first) with unread counts displayed as badges.'

AI generates: a flat list of messages in one global feed. 10 conversations happening simultaneously: messages interleaved, impossible to follow any single thread. With channels + threads: each conversation is isolated, each thread is collapsible, and unread counts tell the user exactly where new activity is. Structure transforms chaos into navigable communication.

Complete Chat and Messaging Rules Template

Consolidated rules for chat and messaging.

  • WebSocket delivery: persistent connection, server pushes, zero polling — 6000x fewer operations
  • Sequence numbers for ordering: server-assigned, deterministic, handles clock skew
  • Read receipts: sent → delivered → read with status icons — IntersectionObserver for read
  • Typing indicators: debounced emit on keystroke, 3s timeout, animated dots display
  • Offline queue: IndexedDB, pending status, auto-send on reconnect, merge by sequence
  • Threading: replyTo field, inline/sidebar/flat display, collapsible threads
  • Channels + DMs: isolated conversations, per-user unread counts, sorted by activity
  • Auto-reconnect: exponential backoff, sync missed messages on reconnect