Guide

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.

  1. Receive the send with its clientMsgId.
  2. Persist the message, and assign messageId and the sequence position.
  3. ACK the sender with the messageId.
  4. 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 clientMsgId turns 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

  1. Why does the sender choose clientMsgId instead of the server? The sender can reuse the same ID on every retry, so the server and the client can recognize a duplicate.
  2. 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 clientMsgId once and reuses it on every retry.
  • The server assigns messageId and 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.