Concepts

Message Receipts

Acknowledgement events that turn raw delivery activity into user-visible sent, delivered, read, and unread states.

intermediate4 min readUpdated unknownModelingDataReliabilityOperationsTradeoffs
Delivery ReceiptRead ReceiptAcknowledgement EventsRead CursorUnread Projection

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:

ReceiptMeaning
SentServer durably accepted the sender's message
DeliveredA recipient device or account acknowledged receipt
ReadThe recipient viewed the message or read up to a sequence
FailedThe 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.

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.