
TL;DR: How Read Receipts Work and How to Build Them
- Read receipts tell a sender that a message has been opened or seen by the recipient. They are different from delivery receipts, which only confirm that a message reached the recipient’s device or inbox.
- A production-ready read receipts system needs four layers: message state, client events, realtime sync, and backend storage. The simplest implementation is usually through a managed chat SDK or chat API.
- For web and JavaScript apps, you typically listen for incoming messages, render unread state, call a “mark as read” API when the conversation is visible, and subscribe to receipt updates.
- For mobile apps, read receipts must handle background state, push notifications, offline sync, notification previews, app lifecycle changes, and privacy settings.
- If you want read receipts without building a realtime messaging stack from scratch, Tencent RTC provides Chat SDKs for web, iOS, Android, Flutter, React Native, and more. See TRTC Chat documentation and the Free Chat API — free forever: 1,000 MAU, no concurrency limits, push notifications included.
What Are Read Receipts and Why They Matter in Modern Chat Apps
Read receipts are message status signals that show whether a recipient has read, opened, or viewed a message. In a one-to-one chat, this may appear as “Seen,” “Read,” a timestamp, or a double-check icon. In a group chat or chat room API, read receipts may show a count such as “Read by 12,” a list of readers, or per-member read status.
In modern chat apps, read receipts are not just cosmetic. They directly shape user trust, response expectations, workflow speed, and engagement. A sales representative wants to know whether a customer has seen a pricing proposal. A doctor in a telehealth app needs assurance that a patient received follow-up instructions. A multiplayer game squad needs to know whether teammates saw a strategy message before a match. A marketplace seller wants to know whether a buyer viewed shipping details.
At a technical level, read receipts are small events attached to a message or conversation. At a product level, they are part of the communication contract between users.
Tencent RTC supports this pattern through TRTC Chat, a cross-platform chat messaging SDK that can be used for private chats, group chats, customer support, social communities, in-app messaging, and realtime collaboration. Developers can use Tencent RTC Chat when they need a managed messaging layer rather than building read receipts, unread counters, push notifications, and message synchronization from scratch.
A simple definition
A read receipt is a realtime signal emitted when a user has opened a message in a conversation context where the app can reasonably infer that the message was visible.
That definition matters because “read” is not always literal. If a push notification preview shows the message on a lock screen, should it count as read? If a user opens a chat list but not the thread, is it read? If a group member scrolls past 200 messages, should all 200 be marked as read? These are product decisions that your chat api and client implementation must support consistently.
Why read receipts improve product experience
Read receipts reduce uncertainty. Messaging is asynchronous, but users still want feedback. Without read receipts, senders often wonder whether a message was lost, ignored, or simply not opened yet.
Common benefits include:
| Use case | Why read receipts matter |
|---|---|
| Social chat | Creates familiar messaging behavior and reduces uncertainty |
| Customer support | Helps agents prioritize follow-up when customers have viewed instructions |
| Team collaboration | Confirms whether operational updates were seen |
| Marketplace chat | Helps buyers and sellers coordinate faster |
| Healthcare and education | Confirms that sensitive instructions were opened |
| Live commerce | Lets hosts and moderators understand participant engagement |
| Gaming | Confirms that team instructions or room invitations were seen |
For apps that combine text chat with calling or live interactions, read receipts can also work alongside voice and video features. For example, a tutoring app might use TRTC Call for live lessons and Chat for lesson materials, homework links, and post-call summaries.
Why not build read receipts manually?
You can build read receipts yourself with WebSockets, a database, and device-specific push notification logic. The WebSocket protocol is standardized by the IETF in RFC 6455, and browser APIs are documented by MDN Web Docs. However, the hard part is not sending one event. The hard part is making message state correct across:
- Multiple devices per user
- Offline users
- Group conversations
- Message history pagination
- Push notifications
- App reinstall and token refresh
- Network reconnection
- Moderation and compliance requirements
- Privacy preferences
- High-volume chat rooms
A managed realtime chat api reduces this complexity. With TRTC Chat, the SDK handles connection management, message delivery, conversation state, unread counts, and platform differences. Developers can focus on product logic and UX.
For complete API reference and platform-specific guides, see the TRTC Chat SDK documentation.
Read Receipts vs Delivery Receipts vs Typing Indicators
Read receipts are often confused with delivery receipts and typing indicators. They are related, but they answer different questions.
Message state comparison
| Signal | Question answered | Typical UI | Trigger |
|---|---|---|---|
| Sent | Did my app submit the message? | Single check or “Sent” | Client successfully sends request |
| Delivered | Did the recipient’s account/device receive it? | Double check or “Delivered” | Server or recipient client acknowledges delivery |
| Read | Did the recipient open or view it? | “Read,” “Seen,” avatar, timestamp | Recipient opens conversation or visible message range |
| Typing | Is the recipient composing a reply? | “Maya is typing…” | Client sends transient typing event |
| Unread count | How many messages are unseen? | Badge count | Server or client calculates unread state |
Delivery receipts
A delivery receipt confirms that a message reached the recipient’s message stream, inbox, device, or account. In some systems, “delivered” means the server accepted the message for that recipient. In stricter systems, it means at least one recipient device acknowledged receipt.
Delivery receipts are useful for diagnosing connectivity and push delivery. They are not proof that a human saw the message.
Read receipts
Read receipts confirm that the recipient opened the conversation or viewed a message range. For one-to-one chats, a common implementation is conversation-level read state: the recipient marks the conversation as read up to the latest message sequence. For group chats, the system may store per-message read members or per-member last-read sequence numbers.
Conversation-level read state is usually more scalable. Instead of writing one database row per user per message, you store each member’s last read message ID or sequence number.
Typing indicators
Typing indicators are ephemeral presence events. They should not be stored like read receipts. A typing event usually expires after a few seconds and should not be replayed from message history.
According to the W3C Web Notifications specification, notification systems are separate from in-app state. That distinction is important: viewing a notification does not necessarily mean reading a message inside the app. Your read receipts policy should define whether notification interactions mark a conversation as read.
Recommended state model
For most apps, use this message lifecycle:
- Sending: message is created locally and queued.
- Sent: chat api accepts the message.
- Delivered: message is available to the recipient account or device.
- Read: recipient opens the conversation and the app marks visible messages as read.
- Archived or deleted: optional product-specific state.
Avoid making read receipts too granular unless your product requires it. Per-message read tracking in large groups can become expensive. A conversation-level “last read message” model is often enough.
Core Architecture: How Read Receipts Work in a Realtime Chat API
A reliable read receipts architecture has six components: user identity, message IDs, conversation state, realtime events, durable storage, and client rendering.
Architecture overview
Sender Client
|
| 1. sendMessage()
v
Chat API / Messaging Server
|
| 2. persist message and assign message ID / sequence
v
Recipient Client(s)
|
| 3. user opens conversation
| 4. markMessageAsRead(conversationID)
v
Chat API / Messaging Server
|
| 5. update lastRead sequence
| 6. emit read receipt event
v
Sender Client updates UICore entities
| Entity | Description | Example |
|---|---|---|
| User | Authenticated chat participant | user_123 |
| Conversation | One-to-one, group, room, or channel | C2C_user_456 |
| Message | Immutable chat item with ID and timestamp | msg_789 |
| Message sequence | Monotonic order value | seq=1042 |
| Read cursor | Last message read by a member | lastReadSeq=1042 |
| Receipt event | Realtime event sent to other clients | conversationRead |
| Unread count | Derived from message sequence and read cursor | unread=5 |
The read cursor pattern
The read cursor is the most important concept in read receipts. Instead of storing a boolean read=true for every message, store a cursor that says:
User A has read conversation C up to message sequence N.
This model has several advantages:
- Fewer writes
- Faster unread count calculation
- Easier multi-device sync
- Better performance in large conversations
- Cleaner pagination behavior
For one-to-one chat, the sender can show “Read” when the recipient’s read cursor is greater than or equal to the message sequence. For group chat, the app can calculate how many members have read up to a message.
Client-side event flow
A robust client should not mark messages as read the moment they arrive. It should mark them as read only when the conversation is actively visible.
A practical rule:
- If the user is inside the conversation, the app is foregrounded, and the message list is visible, mark incoming messages as read.
- If the app is backgrounded, only update unread count.
- If a push notification is tapped and opens the conversation, mark the relevant conversation as read.
- If the user only sees a notification preview, do not mark it as read unless your product explicitly chooses that behavior.
Realtime transport choices
Most realtime chat systems use persistent connections such as WebSocket, long polling fallback, or platform-specific push channels. WebRTC is often used for audio/video media, while WebSocket-style messaging is common for chat control and events. The WebRTC project documents the media and realtime communication stack used by many voice and video applications, but chat message state still typically requires application-level APIs.
TRTC combines realtime communication capabilities across chat, call, live, and conference scenarios. If your app needs messaging plus voice or video, you can pair TRTC Chat with TRTC Call documentation for calling or TRTC Live documentation for live streaming.
Group chat and chat room API considerations
Read receipts become more complex in group chat. You have three common models:
| Model | Best for | Tradeoff |
|---|---|---|
| No group read receipts | Large public rooms | Simple and scalable, less feedback |
| Read count only | Communities, classrooms | Good privacy, moderate complexity |
| Full reader list | Teams, healthcare, enterprise | Maximum clarity, more storage and privacy work |
For very large live chat rooms, full per-message read receipts are usually unnecessary. A read count or unread marker is more practical. For small teams or high-trust enterprise apps, full reader lists can be valuable.
How to Implement Read Receipts with a Chat SDK for Web and JavaScript
This section shows how to implement read receipts using a JavaScript chat sdk web approach. The example uses the Tencent Cloud Chat SDK package and demonstrates initialization, login, message loading, sending messages, marking a conversation as read, and cleanup.
Before you start, create a project and obtain your SDKAppID and UserSig from the TRTC console. You can download SDKs and platform resources from TRTC SDK downloads.
Web implementation checklist
For a web or JavaScript chat api implementation, you need to:
- Initialize the chat SDK.
- Log in the current user.
- Subscribe to message events.
- Load message history for the selected conversation.
- Mark the conversation as read when the thread is visible.
- Update UI when read receipt events or conversation updates arrive.
- Log out and remove listeners when the app closes.
Runnable JavaScript example
Install the SDK:
npm install @tencentcloud/chatCreate chat-read-receipts.js:
import TencentCloudChat from '@tencentcloud/chat';
const SDKAppID = Number(import.meta.env.VITE_TRTC_SDK_APP_ID);
const userID = import.meta.env.VITE_TRTC_USER_ID;
const userSig = import.meta.env.VITE_TRTC_USER_SIG;
// For one-to-one chat, Tencent Cloud Chat conversation IDs commonly use C2C + userID.
const peerUserID = 'user_b';
const conversationID = `C2C${peerUserID}`;
let chat;
function renderMessage(message, currentUserID) {
const mine = message.from === currentUserID;
const text = message.payload?.text || '';
const status = mine ? `status=${message.status || 'sent'}` : '';
console.log(`${mine ? 'Me' : message.from}: ${text} ${status}`);
}
async function initChat() {
chat = TencentCloudChat.create({ SDKAppID });
chat.setLogLevel(1);
chat.on(TencentCloudChat.EVENT.SDK_READY, async () => {
console.log('Chat SDK ready');
const history = await chat.getMessageList({
conversationID,
count: 15
});
history.data.messageList.forEach((message) => {
renderMessage(message, userID);
});
// Mark the conversation as read only after the user opens the thread.
await markConversationAsRead();
});
chat.on(TencentCloudChat.EVENT.MESSAGE_RECEIVED, async (event) => {
const messages = event.data || [];
for (const message of messages) {
renderMessage(message, userID);
if (message.conversationID === conversationID && document.visibilityState === 'visible') {
await markConversationAsRead();
}
}
});
chat.on(TencentCloudChat.EVENT.CONVERSATION_LIST_UPDATED, (event) => {
const conversations = event.data || [];
const current = conversations.find((item) => item.conversationID === conversationID);
if (current) {
console.log('Unread count:', current.unreadCount);
console.log('Last message:', current.lastMessage?.messageForShow);
}
});
await chat.login({ userID, userSig });
}
async function sendText(text) {
const message = chat.createTextMessage({
to: peerUserID,
conversationType: TencentCloudChat.TYPES.CONV_C2C,
payload: { text }
});
const result = await chat.sendMessage(message);
renderMessage(result.data.message, userID);
}
async function markConversationAsRead() {
try {
await chat.setMessageRead({ conversationID });
console.log(`Marked ${conversationID} as read`);
} catch (error) {
console.error('Failed to mark as read:', error);
}
}
async function destroyChat() {
if (!chat) return;
await chat.logout();
chat.destroy();
chat = null;
}
document.addEventListener('visibilitychange', async () => {
if (document.visibilityState === 'visible' && chat) {
await markConversationAsRead();
}
});
window.demoChat = {
initChat,
sendText,
markConversationAsRead,
destroyChat
};Use it in a Vite app:
<button onclick="demoChat.initChat()">Login</button>
<button onclick="demoChat.sendText('Hello with read receipts')">Send</button>
<button onclick="demoChat.markConversationAsRead()">Mark Read</button>
<button onclick="demoChat.destroyChat()">Logout</button>This example is intentionally simple. In production, you should generate UserSig on your backend, not in the browser. You should also debounce setMessageRead() to avoid calling it repeatedly when multiple messages arrive quickly.
React pattern for read receipts
In React, place the markConversationAsRead() call inside an effect that runs when:
- The selected conversation changes
- The SDK is ready
- The browser tab becomes visible
- New messages arrive while the thread is active
A typical React hook structure:
import { useEffect } from 'react';
export function useReadReceipts({ chat, conversationID, isThreadVisible }) {
useEffect(() => {
if (!chat || !conversationID || !isThreadVisible) return;
let cancelled = false;
async function markRead() {
if (cancelled) return;
try {
await chat.setMessageRead({ conversationID });
} catch (error) {
console.error('mark read failed', error);
}
}
markRead();
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') {
markRead();
}
};
document.addEventListener('visibilitychange', onVisibilityChange);
return () => {
cancelled = true;
document.removeEventListener('visibilitychange', onVisibilityChange);
};
}, [chat, conversationID, isThreadVisible]);
}This hook is compatible with a javascript chat sdk architecture and can be adapted to Vue, Angular, Svelte, or plain JavaScript.
Web UX details
On web, avoid marking messages as read when:
- The conversation is open in a hidden browser tab
- The message list is behind a modal
- The user is viewing a different thread
- The app is disconnected and showing stale content
A strong implementation uses both visibility state and route state. For example, document.visibilityState === 'visible' and activeConversationID === message.conversationID.
Mobile Implementation Patterns: Flutter, Swift, React Native, and Android
Mobile read receipts require more lifecycle awareness than web. Phones switch between foreground, background, locked screen, push notification view, split screen, and network transitions. A good chat api for mobile app must handle all of these without creating false read receipts.
Mobile read receipt rules
Use these rules as defaults:
| Situation | Mark as read? | Reason |
|---|---|---|
| User opens conversation screen | Yes | Message is visible |
| User receives push notification | No | Notification does not prove reading |
| User taps notification into conversation | Yes | User entered thread |
| App is foregrounded but user is on chat list | No | Thread not visible |
| User scrolls to a message range | Sometimes | Use visible range for strict read logic |
| App resumes to an open conversation | Yes | Conversation is visible again |
| User disables read receipts | No | Respect privacy setting |
Flutter chat SDK implementation
A Flutter chat sdk should initialize once, log in the user, add message listeners, and mark messages as read when the conversation screen appears.
Add the dependency in pubspec.yaml:
dependencies:
flutter:
sdk: flutter
tencent_cloud_chat_sdk: ^8.0.0Create a conversation screen service:
import 'package:flutter/material.dart';
import 'package:tencent_cloud_chat_sdk/tencent_cloud_chat_sdk.dart';
import 'package:tencent_cloud_chat_sdk/enum/V2TimConversationType.dart';
import 'package:tencent_cloud_chat_sdk/enum/log_level_enum.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_callback.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_message.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_value_callback.dart';
class ChatReadReceiptService {
final int sdkAppID;
final String userID;
final String userSig;
final String peerUserID;
ChatReadReceiptService({
required this.sdkAppID,
required this.userID,
required this.userSig,
required this.peerUserID,
});
Future<void> init() async {
await TencentImSDKPlugin.v2TIMManager.initSDK(
sdkAppID: sdkAppID,
loglevel: LogLevelEnum.V2TIM_LOG_INFO,
listener: V2TimSDKListener(
onConnectSuccess: () => debugPrint('Chat connected'),
onKickedOffline: () => debugPrint('Kicked offline'),
onUserSigExpired: () => debugPrint('UserSig expired'),
),
);
await TencentImSDKPlugin.v2TIMManager.login(
userID: userID,
userSig: userSig,
);
TencentImSDKPlugin.v2TIMManager
.getMessageManager()
.addAdvancedMsgListener(
listener: V2TimAdvancedMsgListener(
onRecvNewMessage: (V2TimMessage message) async {
debugPrint('New message: ${message.msgID}');
if (message.userID == peerUserID) {
await markC2CAsRead();
}
},
),
);
}
Future<List<V2TimMessage>> loadHistory() async {
V2TimValueCallback<List<V2TimMessage>> result =
await TencentImSDKPlugin.v2TIMManager
.getMessageManager()
.getC2CHistoryMessageList(
userID: peerUserID,
count: 20,
);
return result.data ?? [];
}
Future<void> sendText(String text) async {
final createResult = await TencentImSDKPlugin.v2TIMManager
.getMessageManager()
.createTextMessage(text: text);
final messageID = createResult.data?.id;
if (messageID == null) {
throw Exception('Failed to create text message');
}
await TencentImSDKPlugin.v2TIMManager.getMessageManager().sendMessage(
id: messageID,
receiver: peerUserID,
groupID: '',
priority: 0,
onlineUserOnly: false,
isExcludedFromUnreadCount: false,
isExcludedFromLastMessage: false,
);
}
Future<void> markC2CAsRead() async {
V2TimCallback result = await TencentImSDKPlugin.v2TIMManager
.getMessageManager()
.markC2CMessageAsRead(userID: peerUserID);
if (result.code == 0) {
debugPrint('Marked C2C conversation as read');
} else {
debugPrint('Mark read failed: ${result.desc}');
}
}
Future<void> dispose() async {
await TencentImSDKPlugin.v2TIMManager.logout();
TencentImSDKPlugin.v2TIMManager.unInitSDK();
}
}Use it in a StatefulWidget:
@override
void initState() {
super.initState();
service.init().then((_) async {
await service.loadHistory();
await service.markC2CAsRead();
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
service.markC2CAsRead();
}
}This pattern avoids marking messages as read while the app is only receiving background push notifications.
Swift implementation pattern
For a chat sdk swift implementation, the same principles apply:
- Initialize the SDK in app startup.
- Log in after your backend returns a user token or UserSig.
- Add an advanced message listener.
- Mark C2C or group messages as read in
viewDidAppear. - Avoid marking as read from push notification receipt alone.
A simplified Swift-style flow:
import UIKit
import ImSDK_Plus
final class ChatViewController: UIViewController, V2TIMAdvancedMsgListener {
let peerUserID = "user_b"
override func viewDidLoad() {
super.viewDidLoad()
V2TIMManager.sharedInstance().addAdvancedMsgListener(listener: self)
loadMessages()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
markAsRead()
}
func loadMessages() {
V2TIMManager.sharedInstance().getC2CHistoryMessageList(
userID: peerUserID,
count: 20,
lastMsg: nil,
succ: { messages in
print("Loaded messages: \(messages?.count ?? 0)")
},
fail: { code, desc in
print("Load failed: \(code), \(desc ?? "")")
}
)
}
func onRecvNewMessage(msg: V2TIMMessage!) {
if msg.userID == peerUserID && view.window != nil {
markAsRead()
}
}
func markAsRead() {
V2TIMManager.sharedInstance().markC2CMessage(asRead: peerUserID, succ: {
print("Marked as read")
}, fail: { code, desc in
print("Mark read failed: \(code), \(desc ?? "")")
})
}
deinit {
V2TIMManager.sharedInstance().removeAdvancedMsgListener(listener: self)
}
}For production iOS apps, connect this with UIApplication lifecycle callbacks so that read receipts are emitted only when the conversation is visible and the app is active.
React Native chat api pattern
A react native chat api implementation usually mirrors Flutter:
- Initialize in a top-level provider
- Store active conversation in state
- Mark as read on screen focus
- Use
AppStateto detect foreground/background - Debounce read calls
- Keep push notification handling separate
If you use React Navigation, call markAsRead() inside useFocusEffect. If the user opens a notification, navigate to the conversation screen first, then mark the thread as read after the screen is mounted.
Android implementation pattern
For Android, combine:
- Activity or Fragment lifecycle
- ViewModel state
- Message listener
- Push notification click intent
onResume()mark-read logic
For the best chat api for android, look for SDK support for offline message sync, unread count, push integration, and group conversation read state. Android apps are often killed and restarted by the OS, so your implementation should recover from cached conversation state and request fresh unread counts after reconnect.
Voice chat api and read receipts
A voice chat api is not the same as a text chat api, but many social, gaming, and community apps need both. For example, a multiplayer game may use Tencent RTC GVoice for in-game voice and Chat for text channels, room invitations, and read receipts. This separation keeps media transport and message state optimized for their own workloads.
Backend Considerations: Laravel Chat API, Message States, and Data Storage
Even when you use a managed chat messaging SDK, your backend still matters. Your server usually owns authentication, user profiles, business permissions, audit logs, and sometimes message metadata.
What your backend should own
| Backend responsibility | Why it matters |
|---|---|
| User authentication | Prevents impersonation |
| UserSig or token generation | Keeps secrets out of clients |
| Role and permission checks | Controls who can join conversations |
| Conversation metadata | Supports business workflows |
| Webhooks or callbacks | Syncs message events to your system |
| Compliance logs | Required in regulated industries |
| Privacy preferences | Controls whether read receipts are enabled |
Never expose secret keys in client-side JavaScript, mobile apps, or public repositories. Generate short-lived credentials on your backend.
Laravel chat api example for secure UserSig distribution
A laravel chat api often acts as a thin authentication layer in front of the chat sdk. The frontend asks your backend for a token. The backend verifies the logged-in user and returns a UserSig.
This example shows the structure. Replace buildUserSig() with your official server-side UserSig generation implementation.
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Auth;
Route::middleware('auth:sanctum')->get('/chat/credential', function (Request $request) {
$user = Auth::user();
$sdkAppId = (int) config('services.trtc.sdk_app_id');
$secretKey = config('services.trtc.secret_key');
return response()->json([
'sdkAppID' => $sdkAppId,
'userID' => (string) $user->id,
'userSig' => buildUserSig($sdkAppId, $secretKey, (string) $user->id),
'expiresIn' => 3600,
]);
});
function buildUserSig(int $sdkAppId, string $secretKey, string $userId): string
{
// Use Tencent RTC's official UserSig generation library in production.
// This placeholder intentionally avoids exposing signing internals.
// Keep SECRET_KEY only on the server.
return app(\App\Services\TencentUserSigService::class)
->generate($sdkAppId, $secretKey, $userId, 3600);
}Then your JavaScript app calls:
async function fetchChatCredential() {
const response = await fetch('/api/chat/credential', {
headers: {
Authorization: `Bearer ${localStorage.getItem('app_token')}`
}
});
if (!response.ok) {
throw new Error('Failed to fetch chat credential');
}
return response.json();
}This backend pattern works for web, Flutter, Swift, React Native, and Android clients.
Database design if you store read state yourself
If your product requires custom analytics or compliance records, you may store your own read state in addition to the managed chat api. A scalable schema uses conversation cursors.
Example tables:
CREATE TABLE conversation_members (
conversation_id VARCHAR(128) NOT NULL,
user_id VARCHAR(128) NOT NULL,
last_read_message_id VARCHAR(128),
last_read_seq BIGINT DEFAULT 0,
last_read_at TIMESTAMP NULL,
read_receipts_enabled BOOLEAN DEFAULT TRUE,
PRIMARY KEY (conversation_id, user_id)
);
CREATE TABLE message_audit_log (
message_id VARCHAR(128) PRIMARY KEY,
conversation_id VARCHAR(128) NOT NULL,
sender_id VARCHAR(128) NOT NULL,
message_seq BIGINT NOT NULL,
sent_at TIMESTAMP NOT NULL
);For most apps, avoid writing a row for every message-reader pair. In a group with 10,000 members, a single message could create 10,000 receipt rows. That model becomes expensive quickly.
Message states and idempotency
Read receipt APIs should be idempotent. If a client calls markAsRead(conversationID, seq=100) five times, the final state should still be lastReadSeq=100. If a later call marks seq=120, do not allow an older retry to move the cursor backward to 100.
Use monotonic updates:
UPDATE conversation_members
SET last_read_seq = GREATEST(last_read_seq, :new_seq),
last_read_at = CASE
WHEN :new_seq >= last_read_seq THEN NOW()
ELSE last_read_at
END
WHERE conversation_id = :conversation_id
AND user_id = :user_id;Webhooks and analytics
If your chat provider supports webhooks or server callbacks, you can record read receipt events for analytics. Useful metrics include:
- Average time to read
- Read rate by conversation type
- Support agent response after customer read
- Drop-off after unread messages
- Push notification open-to-read conversion
Be careful with privacy. Read behavior can be sensitive personal data, especially in healthcare, workplace, education, and finance apps. The GDPR text published by the European Union defines principles around personal data processing that may apply depending on your users and region. Consult legal counsel for compliance requirements.
UX, Privacy, and Performance Best Practices for Read Receipts
Read receipts are powerful, but they can also create pressure. A good implementation balances transparency, user control, and performance.
UX best practices
Use clear labels
“Read” and “Delivered” should mean different things. Do not use a read icon for delivery.
Show timestamps when helpful
In business messaging, “Read at 14:03” can be useful. In casual chat, a subtle icon may be enough.
Avoid noisy UI in groups
For large groups, show a count instead of a long reader list.
Handle offline state gracefully
If a user is offline, show “Sent” or “Delivered” based on actual server state. Do not guess.
Make unread markers obvious
A “New messages” divider helps users understand what was unread since their last visit.
Do not mark hidden content as read
If messages are loaded below the fold, decide whether opening the thread is enough or whether visibility tracking is required.
Privacy best practices
Read receipts can expose behavior. Some users do not want others to know when they read a message. This is especially important in dating apps, mental health apps, workplace messaging, and communities involving sensitive topics.
Recommended privacy controls:
| Control | Recommendation |
|---|---|
| Disable read receipts | Offer user-level setting when appropriate |
| One-to-one visibility | Let users hide read status from individual chats |
| Group visibility | Prefer read counts over reader lists in large groups |
| Admin override | Use only for compliance-heavy enterprise scenarios |
| Notification previews | Do not mark as read by default |
| Data retention | Do not keep read logs longer than needed |
If your app offers a setting to disable read receipts, define whether it is reciprocal. Many consumer chat apps make it reciprocal: if you turn off your read receipts, you also cannot see others’ read receipts.
Performance best practices
Read receipts can create event storms. Imagine a chat room where 5,000 users open the same conversation at once. If every client emits per-message read events, your system can overload.
Use these performance patterns:
- Debounce mark-read calls: wait 300–1000 ms before sending.
- Use read cursors: update last read sequence, not every message.
- Batch updates: mark a range as read instead of one message at a time.
- Avoid full reader lists in large groups: use counts or disable group receipts.
- Cache unread counts: avoid recalculating on every page render.
- Use pagination: load message history in chunks.
- Handle reconnects: sync read state after network recovery.
Common mistakes and fixes
| Mistake | Impact | Fix |
|---|---|---|
| Marking as read on push receipt | False read receipts | Mark only when user opens conversation |
| Storing one row per user per message | High database cost | Store per-member read cursor |
| Calling mark-read for every incoming message | Excess API calls | Debounce and batch |
| Ignoring multi-device users | Inconsistent state | Sync read cursor across devices |
| No privacy setting | User trust issue | Add user or conversation-level control |
| Moving read cursor backward | Incorrect unread count | Use monotonic updates |
| Treating typing as persistent | Stale UI | Expire typing indicators quickly |
Accessibility considerations
Read receipt UI should not rely only on color. A gray check and blue check may be hard to distinguish. Use text labels or accessible names such as aria-label="Read" in web apps. Screen reader users should be able to understand message state without visual icons.
Security considerations
Secure chat implementation includes more than read receipts. Protect credentials, validate permissions, and avoid leaking message metadata to unauthorized users. If you are building an api for chat application, your backend should verify that a user belongs to a conversation before returning message history or read status.
For apps that require realtime audio/video alongside chat, review TRTC Conference documentation for meeting scenarios and TRTC Conversational AI documentation if you are building AI voice interactions.
Pro Tip: If you're building AI-powered voice interactions, Tencent RTC's Conversational AI provides low-latency speech interaction capabilities for natural conversations.
Choosing a Free Chat SDK or Chat API with Read Receipts Support
Choosing the best chat api is not only about read receipts. You need to evaluate platform coverage, scalability, moderation, push notifications, pricing, SDK quality, and how fast your team can ship.
Selection matrix
| Requirement | Why it matters | What to look for |
|---|---|---|
| Read receipts | Core messaging feedback | Conversation read state, group read support |
| Delivery receipts | Message reliability | Sent, delivered, failed states |
| Unread counts | Navigation and engagement | Conversation list unread counters |
| Push notifications | Mobile re-engagement | APNs, FCM, vendor push integrations |
| Web SDK | Browser apps | chat sdk web, JavaScript support |
| Mobile SDKs | Native apps | iOS, Android, Flutter, React Native |
| Group chat | Communities and teams | Member management, roles, mute, moderation |
| Chat room API | Live events | High concurrency and message control |
| Backend APIs | Business integration | REST APIs, callbacks, webhooks |
| Security | User trust | Token-based login, permissions |
| Pricing | Long-term cost | Free tier and predictable scaling |
| Documentation | Developer speed | Clear guides and runnable demos |
Tencent RTC is a strong option when you need a cross-platform in app chat sdk with room to expand into calling, live streaming, conference, AI, or gaming voice. Start with TRTC Chat for messaging, use TRTC Call for 1v1 and group calls, add TRTC Live for interactive live streaming, and use GVoice for gaming voice scenarios.
Free Chat API — free forever: 1,000 MAU, no concurrency limits, push notifications included.
Free chat api evaluation checklist
Before choosing a free chat sdk, ask:
- Does the free plan include read receipts or only basic messaging?
- Are push notifications included?
- Are there concurrency limits?
- Which platforms are supported?
- Can I use the same chat api javascript implementation on web and backend-assisted apps?
- Does the SDK support Flutter, Swift, Android, and React Native?
- Is group chat supported?
- Is there a chat room api for live communities?
- Can I export or audit message events?
- How are credentials generated and protected?
- Is documentation complete enough for my team?
TRTC’s Free Chat API is useful for early-stage apps because it is not merely a short trial. It supports a free-forever model with 1,000 MAU, no concurrency limits, and push notifications included. That makes it practical for prototypes, MVPs, internal tools, student projects, and early production launches.
When to build yourself
Building your own realtime chat api may make sense if:
- Messaging is not central to your product
- You only need basic comments or notifications
- Your team already operates realtime infrastructure
- You need an unusual protocol or data model
- You have strict internal platform constraints
However, for most product teams, a managed chat messaging SDK is faster and safer. Read receipts seem simple at first, but the edge cases multiply as soon as you add multiple devices, offline sync, push, group chats, and privacy controls.
Accelerate Integration with MCP
Instead of reading documentation page by page, use Tencent RTC's MCP server to let your AI coding assistant generate integration code directly:
Setup (Cursor / VS Code / Claude Code):
{
"mcpServers": {
"tencent-rtc": {
"command": "npx",
"args": ["-y", "@tencent-rtc/mcp@latest"],
"env": {
"SDKAPPID": "YOUR_SDKAPPID",
"SECRETKEY": "YOUR_SECRET_KEY"
}
}
}
}Example prompts you can use:
- "Create a video calling app using TRTC Web SDK with Vue 3"
- "Integrate real-time chat into my React app with message history and read receipts"
- "Add live streaming to my existing Express backend"
- "Generate a Flutter chat screen that marks C2C messages as read on view focus"
- "Create a Laravel endpoint that returns TRTC Chat credentials securely"
The MCP server has access to TRTC SDK documentation and can generate working code with your credentials pre-filled. For the full MCP setup guide, see the official MCP documentation.
💡 Pro Tip for AI-assisted development: If you use Cursor or CodeBuddy, the TRTC MCP server (
@tencent-rtc/mcp) can scaffold your real-time communication layer in minutes — from project setup to credential handling to working chat and video calls.
Implementation Checklist for Production Read Receipts
Use this checklist before launching read receipts in production.
Product checklist
- Define what “read” means in your app.
- Decide whether notification previews count as read.
- Decide whether users can disable read receipts.
- Define one-to-one behavior separately from group behavior.
- Choose between read count and reader list for groups.
- Add accessible labels for read states.
- Document behavior in privacy policy if needed.
Engineering checklist
- Use stable message IDs or sequence numbers.
- Store read cursor per conversation member.
- Make mark-read idempotent.
- Prevent read cursor rollback.
- Debounce client read calls.
- Sync read state across devices.
- Handle offline and reconnect flows.
- Keep credentials on backend.
- Test with slow networks and background app state.
- Monitor read event volume.
QA scenarios
| Scenario | Expected behavior |
|---|---|
| User opens a one-to-one chat | Conversation unread count becomes zero |
| User receives message in background | Unread count increases, no read receipt |
| User taps push notification | App opens thread, then marks as read |
| User opens same account on two devices | Read cursor syncs across devices |
| User disables read receipts | App stops publishing read state |
| Network disconnects during mark-read | App retries safely without duplicate state |
| Group has 1,000 members | UI uses count or scalable model |
| Sender deletes a message | Read state does not break message list |
FAQ About Read Receipts and Chat APIs
What are read receipts in a chat app?
Read receipts are status signals that tell a sender when a recipient has opened or viewed a message. They usually appear as “Read,” “Seen,” a timestamp, an avatar, or a read count in group chats.
Are read receipts the same as delivery receipts?
No. Delivery receipts confirm that a message reached the recipient’s account, device, or inbox. Read receipts confirm that the recipient opened the conversation or viewed the message according to your app’s rules.
What is the best way to implement read receipts?
The most scalable approach is to use a read cursor. Store the latest message sequence each user has read in each conversation. Then calculate whether specific messages are read based on that cursor. A managed chat sdk can provide this behavior without requiring you to build realtime infrastructure.
Can I implement read receipts with a JavaScript chat SDK?
Yes. With a javascript chat sdk, initialize the SDK, log in the user, load message history, listen for new messages, and call the SDK’s mark-read method when the conversation is visible. The example above shows this pattern using Tencent Cloud Chat.
How do read receipts work in Flutter?
In Flutter, initialize the chat SDK, add a message listener, and call markC2CMessageAsRead or the relevant group read API when the conversation screen appears or resumes. Do not mark messages as read only because a push notification arrived.
Should group chats show everyone who read a message?
It depends on the product. Small enterprise groups may benefit from full reader lists. Large communities and live chat rooms should usually show read counts or avoid group read receipts to reduce storage, event volume, and privacy concerns.
Do read receipts create privacy risks?
Yes. Read receipts can reveal user behavior and availability. Consider adding a setting to disable them, avoid marking notification previews as read, and minimize how long you store read event logs.
Is there a free chat API that supports read receipts?
Yes. Tencent RTC offers a Free Chat API that is free forever with 1,000 MAU, no concurrency limits, and push notifications included. It is a practical starting point for apps that need messaging, unread counts, and read receipt behavior.
Conclusion: Build Read Receipts That Users Trust
Read receipts are small signals with a large impact. They help users understand whether messages were seen, reduce uncertainty, and make chat apps feel responsive. But they must be implemented carefully. A production-grade read receipts system needs clear product rules, correct message state, realtime updates, privacy controls, and mobile lifecycle handling.
If you are building from scratch, use read cursors, idempotent updates, debounced client calls, and strict backend permission checks. If you want to ship faster, use a managed chat api with cross-platform SDKs, unread counts, push notifications, and read state built in.
To start building, explore Tencent RTC Chat, review the TRTC Chat SDK documentation, and try the Free Chat API for a free-forever messaging foundation. For apps that need communication beyond text, combine Chat with TRTC Call, TRTC Live, or Tencent RTC GVoice.
Author
Maya Chen is a developer content strategist focused on realtime communication, chat infrastructure, and SDK integration patterns. She writes practical engineering guides for teams building messaging, voice, video, and live interactive applications.


