Concepts
Message Receipts
Acknowledgement events that turn raw delivery activity into user-visible sent, delivered, read, and unread states.
Concepts Covered
- Sent state
- Delivered receipts
- Read receipts
- Read cursors
- Unread counts
- Receipt batching
- Group receipt fan-out
- Receipt projection drift
Definition
Message receipts are acknowledgement events that describe what happened after a message was accepted.
They power familiar chat states such as sent, delivered, read, and unread. These labels look small in the UI, but they require careful modeling.
"The server accepted my message" is different from "the recipient device received it," and both are different from "the recipient opened the conversation."
Receipts should be modeled as events that update derived state, not as perfect real-time truth.
The Pain That Forces Receipt Modeling
Users care about progress. After sending a message, they want to know whether it left the phone, reached the server, reached the recipient, or was read.
The backend sees a less clean story:
sender sends message
server stores it
recipient phone is offline
laptop receives it
phone receives it later
user reads on laptop
phone syncs read state later
If the system uses one boolean like message_seen = true, it loses important meaning. Which device saw it? Was it delivered or read? Did privacy settings allow the sender to know? Did the unread count update everywhere?
Receipts exist because delivery progress is multi-stage, multi-device, and eventually consistent.
Types Of Receipts
Common receipt types:
| Receipt | Meaning |
|---|---|
| Sent | Server durably accepted the sender's message |
| Delivered | A recipient device or account acknowledged receipt |
| Read | The recipient viewed the message or read up to a sequence |
| Failed | The system could not accept or deliver according to product policy |
Different products define these differently. One product may show delivered when any recipient device receives the message. Another may wait until a primary device receives it. Group chats may show aggregate receipt state rather than individual details.
Receipts As Events
Receipts fit naturally into an event model:
message_delivered(
message_id=m_7,
user_id=u_20,
device_id=d_3,
delivered_at=...
)
conversation_read(
conversation_id=c_10,
user_id=u_20,
read_up_to_sequence=84211,
read_at=...
)
The second form is important. For read receipts, it is often cheaper and clearer to say "this user read up to sequence 84211" than to emit one read event per message.
Why Read Cursors Matter
A read cursor compresses many read events into one durable position.
Instead of:
read message 100
read message 101
read message 102
the client can say:
read_up_to_sequence = 102
That single cursor can drive unread counts, read markers, and conversation state.
The cursor must move carefully. It should usually advance forward, not backward. If old delayed updates arrive after newer ones, the system should not accidentally mark the user as less caught up than they already are.
Why Receipts Are Eventually Consistent
Receipts arrive through unreliable clients and asynchronous pipelines.
A recipient may read a message while offline. A device may batch read state and send it later. A worker may process receipt events with lag. A projection may update unread counts a moment after the source event is stored.
That means receipts are usually eventually consistent. The source message should be durable first. The receipt state catches up.
This is acceptable when the UI is honest. Users generally tolerate a read marker appearing slightly late. They do not tolerate losing the actual message.
Unread Counts Are Derived State
Unread counts are not usually stored as a single permanent truth. They are derived from messages, read cursors, membership, mute/archive settings, and sometimes per-device state.
Example:
unread_count = messages_after(read_up_to_sequence)
At scale, the system stores an inbox projection because computing this from raw messages on every app open would be expensive. That projection can drift and needs repair paths.
Operational Reality
Important signals:
- receipt event rate
- receipt processing lag
- duplicate receipt count
- read cursor regressions
- unread projection drift
- group receipt fan-out volume
- privacy-filtered receipt count
- delayed receipt age
Failure modes:
- The sender UI shows delivered before a device actually acknowledges.
- Read receipts are duplicated because retries are not idempotent.
- Unread counts drift after missed receipt events.
- Group read state creates huge write amplification.
- Privacy settings are ignored and read state leaks.
- A delayed receipt makes a conversation appear unread after the user already read it.
Related Topics
Knowledge links
Use these links to understand what to know first, where this idea appears, and what to study next.
Prerequisites
Read these first if this topic feels unfamiliar.
Used In Systems
System studies where this idea appears in context.
Related Concepts
Core ideas that connect to this topic.
Related Patterns
Reusable architecture moves built from these ideas.