Case studies
Client message IDs
Networks retry, so a chat message can arrive twice. The client assigns an ID before sending, the server stores it on the message, and duplicates are dropped by that ID.
A chat app can't tell whether a send worked. The request might have reached the server and the reply got lost. The app then retries, and the message could appear twice. Chat avoids this with two IDs. The client sets a clientMsgId before sending. The server assigns a canonical messageId once it saves the message.
Concept
The system delivers at least once, so a message can arrive more than once after retries. A UUID is a random identifier that is almost certainly unique. Read delivery semantics if the trade-off is new to you.
The big idea
The sender knows its own message. The server knows the order the messages were saved in. Each side gets an ID that matches its job.
The clientMsgId is what makes a retry safe. The sender creates it once and reuses it on every retry. The server treats it like an idempotency key, so a second copy of the same send is recognized rather than stored again.
Walking through it
The client-to-server flow is short.
CLIENT MSG_SEND { channelId, clientMsgId, body }
SERVER MSG_ACK { clientMsgId, messageId, ts }
SERVER MSG_RECV { messageId, channelId, senderId, body, ts } ← to recipients
CLIENT MSG_DELIVERED { messageId } / MSG_READ { messageId }The server handles a send in this order.
- Receive the send with its
clientMsgId. - Persist the message, and assign
messageIdand the sequence position. - ACK the sender with the
messageId. - Fan out to the recipients' devices.
The order of steps 2 and 3 matters. If the server ACKs first and then crashes, the sender believes the message was saved, and it wasn't. Persisting first means the worst case is a duplicate, and the clientMsgId handles that.
A crash after the save but before fan-out is also safe. Recipients catch up through sync, which is covered in the message ordering page.
Common mistakes
- ACKing before saving. A crash then loses a message the sender thinks went through.
- Changing the ID on retry. A new
clientMsgIdturns a retry into a visible duplicate. - Deduplicating on timestamps. Two real messages can share a timestamp, and retries can arrive later than they were sent.
Check yourself
- Why does the sender choose
clientMsgIdinstead of the server? The sender can reuse the same ID on every retry, so the server and the client can recognize a duplicate. - Why persist the message before ACKing the sender? If the server ACKs first and crashes, the sender believes a message was saved when it wasn't.
Key takeaways
- Delivery is at least once, so duplicates are expected.
- The client sets
clientMsgIdonce and reuses it on every retry. - The server assigns
messageIdand persists before the ACK. - Clients drop duplicates by ID, so each message appears once.
Further learning
The idempotency page explains how a server stores results under a key so retries are safe.
Next, read about presence and typing, which covers the signals that are allowed to be lost.
Related
Case study: chat system
Design a WhatsApp or Slack style messaging system. Connection gateways, per-conversation ordering, wide-column storage and large-channel fan-out are the parts that matter.
Delivery semantics
The three promises a messaging system can make (at-most-once, at-least-once, exactly-once) and why the real choice is when the consumer says "done".
Idempotency and retries
Making operations safe to retry, with idempotency keys, deduplicating consumers and jittered backoff, so at-least-once delivery never means a double charge.