Patterns
Read Cursor Receipts
Represent read state as a user's latest read sequence in a conversation instead of writing one read receipt per message.
Concepts Covered
- Read receipts
- Read cursors
- Read-up-to sequence
- Unread projections
- Receipt batching
- Projection drift
- Reconciliation
- Multi-device read state
1. Intent
The Read Cursor Receipts pattern stores read state as "this user has read up to sequence N" rather than writing one read record for every message.
In chat systems, users often read a batch of messages at once. If a user opens a conversation with 40 unread messages, the system usually does not need 40 separate read events.
One cursor can express the same product state:
user u_7 read conversation c_10 up to sequence 84211
This reduces write volume and makes unread count computation easier.
2. The Problem Without This Pattern
Per-message read receipts create heavy write amplification.
Imagine a group with 10,000 members and a burst of 100 messages. If every member eventually reads every message and the system records one row per message per user, the receipt table grows quickly.
The write path, event stream, projection workers, and unread counters all absorb that load.
Most chat UIs only need to know the latest contiguous position a user has read. A cursor captures that directly.
3. How The Pattern Works
The message log has ordered sequences:
message m_1 -> sequence 100
message m_2 -> sequence 101
message m_3 -> sequence 102
The receipt service stores:
conversation_read_state
- user_id
- conversation_id
- read_up_to_sequence
- read_at
When the user opens the conversation, the client sends:
mark_read(conversation_id=c_10, read_up_to_sequence=84211)
The server validates that the user is a member and that the sequence exists. It then updates the cursor if the new sequence is ahead of the old one.
The update should usually be monotonic:
new_cursor = max(existing_cursor, requested_cursor)
This prevents stale devices from moving read state backward.
4. When To Use It
Use this pattern when:
- read state is mostly sequential
- unread counts matter
- per-message receipts would be too expensive
- conversations have ordered sequences
- clients can batch read updates
- read state feeds derived projections
- users may read multiple messages at once
It is especially useful for inboxes, chat conversations, notification feeds, and activity streams.
5. When Not To Use It
Avoid this pattern when:
- users can read arbitrary messages out of order and the product must track exact gaps
- every message requires an individual audit acknowledgement
- legal or compliance rules require per-item read evidence
- there is no stable ordering boundary
- read state should remain purely local to one device
You can combine read cursors with exceptions, but that adds complexity.
6. Data And Operational Model
Core row:
read_state
- user_id
- conversation_id
- read_up_to_sequence
- updated_at
Derived projection:
inbox_projection
- user_id
- conversation_id
- unread_count
- latest_message_sequence
- read_up_to_sequence
Operators should monitor:
- receipt event lag
- unread projection drift
- read update rate
- duplicate read events
- cursor regression attempts
- reconciliation corrections
- privacy-filtered read receipts
The read cursor may be user-level or device-level depending on product rules. If reading on laptop should clear unread on phone, user-level cursor is natural. If each device keeps independent read state, device-level cursor is needed.
7. Failure Modes
- A stale client moves the cursor backward.
- A client marks messages read before rendering them safely.
- Unread counts drift because projection workers miss receipt events.
- Membership changes make a cursor point to messages the user should not see.
- A read receipt leaks privacy state when the user disabled read receipts.
- Concurrent devices race to update read state.
- Cursor update is accepted for a sequence that does not belong to the conversation.
8. Tradeoffs
| Benefit | Cost |
|---|---|
| Reduces receipt write amplification | Does not capture arbitrary read gaps |
| Makes unread counts cheaper | Requires ordered conversation sequences |
| Supports batching naturally | Projection drift still needs repair |
| Works well across devices | Product semantics must be precise |
| Simplifies read state | Needs monotonic update rules |
Read cursor receipts compress a noisy stream of per-message actions into one meaningful progress marker.
9. Related Systems And Concepts
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.