PubSub - Android
PubSub (publish-subscribe) sends messages on named topics. A participant publishes a message to a topic, and every participant subscribed to that topic receives it.
Here is how the flow looks.

For frequent updates that don't need a topic or history, such as cursor positions, the Data Channel is lighter than PubSub.
pubSub
Use room.pubSub, the room's PubSub object, to publish and subscribe to any topic.
publish()
publish() sends a message to a topic. It takes:
topic: the topic to publish to.message: the message, as aString.options(optional): aPubSubPublishOptions. Pass its two options to the constructor as named arguments, for examplePubSubPublishOptions(persist = true, sendOnly = arrayOf("participantId")). Both are optional.persist:truekeeps the message for the rest of the session. Participants who subscribe later receive it ononOldMessagesReceived(), and it is available as a.csvfile in the VideoSDK Session Dashboard after the session. Defaults tofalse, which delivers the message only to the participants subscribed right now.sendOnly: theparticipantIds of the participants who should receive the message, as anArray<String>.null(the default) or an empty array sends it to every subscriber.
payload(optional): extra data to send with the message, as aJSONObject. It comes afteroptions, so pass it last or as a named argument.
private fun sendMessage() {
// publish message
val options = PubSubPublishOptions(persist = true)
lifecycleScope.launch {
try {
room!!.pubSub.publish("CHAT", "Hello Everyone!", options)
} catch (e: VideoSDKException) {
// Room is not joined, is reconnecting, or the message could not be sent
Log.d("VideoSDK", "publish failed: ${e.code} ${e.message}")
}
}
}
subscribe()
subscribe() starts delivering a topic's messages to a listener. It takes:
-
topic: the topic to subscribe to. -
listener: aPubSubMessageListenerwith the following callbacks. Each callback has an empty default body, so override only the ones you need.-
onMessageReceived(message)— Called for every live message on the topic, in publish order, including the messages you publish yourself.- Use this when: You want to handle each message individually, such as appending it to a chat, triggering a notification, or updating message-specific state.
- Don't use this when: The topic is high-throughput / large-scale (busy chats, live reactions, cursor positions). Firing once per message causes excessive UI updates — use
onBatchReceived()instead.
-
onBatchReceived(messages)— Called once per live batch with all messages of that batch. These are the same messages delivered throughonMessageReceived(), only grouped — use one path or the other, not both.- Use this when: Handling messages in bulk is cheaper, such as one UI update or one database transaction per batch instead of one per message.
- Note: This callback fires only when batching is turned on for the room on the server side, so your app must work with and without it. Every message of a batch also arrives on
onMessageReceived(). Handle each message in one callback or the other, never both, or you will count it twice.
-
onOldMessagesReceived(messages, info)— Called with the topic's persisted history in batches, once per batch with a partial list. Only messages published withpersistenabled are delivered here. Append each batch to what you already hold rather than replacing it.info.isLastistrueon the final batch. A topic with no persisted history never fires this callback.- Use this when: Loading chat history or restoring previously sent messages when a participant joins the room, or hiding a loader once the full history is rendered.
-
onMessageDrop(info)— Called when some live messages on the topic were not delivered to this participant. This can happen:- because this participant fell further behind than
maxQueueallows underQUEUE, or - because
newMessageLimitis set, which caps how many live messages this participant accepts every 500 ms. Messages over the cap are skipped.
info.droppedCountis the number of messages that were not delivered. One callback can cover many messages. A loss is reported only once delivery resumes, so messages skipped at the very end of a burst are never reported. This callback never fires whenrealtimeOverflowisDROP.- Use this when: Notifying users that some realtime messages were missed, monitoring the effects of
newMessageLimit, or logging dropped messages for debugging and observability.
- because this participant fell further behind than
-
-
options(optional): aPubSubSubscribeOptions.PubSubSubscribeOptionshas 4 options, which you pass to its constructor as named arguments, for examplePubSubSubscribeOptions(oldMessageLimit = 100, maxQueue = 70). All are optional. They are read-only properties afterwards.oldMessageLimit: how many persisted messages are delivered when you subscribe. Passnullfor all history,0for none, orNfor the last N messages. Defaults tonull.realtimeOverflow: what happens when live messages arrive faster than they can be delivered. It is aRealtimeOverflow.QUEUEbuffers them and delivers them paced, dropping the oldest on overflow;DROPkeeps only the newest messages of each delivery window. Defaults toRealtimeOverflow.QUEUE.maxQueue: how far this participant may fall behind underQUEUE, counted in delivery batches. Messages beyond it are reported ononMessageDrop(). It must be1or more and is only valid withQUEUE: combining it withDROPmakessubscribe()throw aVideoSDKExceptionwith code3073. If you don't provide any value, the server decides. The first participant to subscribe to a topic sets it for everyone.newMessageLimit: the most live messages this participant accepts every 500 ms.null(the default) or0means unlimited.
A PubSubMessage has the following properties. They are nullable, so read them with a fallback, for example message.message ?: "".
id: the message's unique ID. TypeString?.message: the message that was sent. TypeString?.senderId: theparticipantIdof the sender. Compare it withroom.localParticipant.idto recognise your own messages. TypeString?.senderName: thedisplayNameof the sender. TypeString?.timestamp: when the server accepted the message, as an ISO-8601 UTC string such as2026-10-05T09:30:00.000Z. It is""when the server sent none. TypeString.topic: the topic the message was published to. TypeString?.payload: theJSONObjectsent with the message, ornullif there was none. TypeJSONObject?.
Call subscribe() once the room is joined. The example below runs in an activity opened after the room is joined. If the same activity joins the room, subscribe from onRoomJoined() instead, as shown in Raise Hand using PubSub. Called before the join completes, subscribe() throws 3035.
val pubSubMessageListener = object : PubSubMessageListener {
override fun onMessageReceived(message: PubSubMessage) {
Log.d("#message", "onMessageReceived: ${message.message}")
}
override fun onOldMessagesReceived(messages: List<PubSubMessage>, info: PubSubHistoryInfo) {
Log.d("#message", "onOldMessagesReceived: ${messages.size} messages, isLast: ${info.isLast}")
}
override fun onBatchReceived(messages: List<PubSubMessage>) {
Log.d("#message", "onBatchReceived: ${messages.size} messages")
}
override fun onMessageDrop(info: PubSubMessageDropInfo) {
Log.d("#message", "onMessageDrop: dropped ${info.droppedCount} messages")
}
}
override fun onCreate(savedInstanceState: Bundle?) {
//...
// Subscribe for 'CHAT' topic
lifecycleScope.launch {
try {
room!!.pubSub.subscribe("CHAT", pubSubMessageListener)
} catch (e: VideoSDKException) {
// Room is not joined, is reconnecting, or the subscription failed
Log.d("VideoSDK", "subscribe failed: ${e.code} ${e.message}")
}
}
}
unsubscribe()
unsubscribe() stops delivering a topic's messages to a listener. It takes:
topic: the topic to unsubscribe from.listener: thePubSubMessageListeneryou passed tosubscribe().
fun unsubscribe() {
// Unsubscribe 'CHAT' topic
lifecycleScope.launch {
try {
room!!.pubSub.unsubscribe("CHAT", pubSubMessageListener)
} catch (e: VideoSDKException) {
// Room is not joined (or has been left), is reconnecting, or the server call failed
Log.d("VideoSDK", "unsubscribe failed: ${e.code} ${e.message}")
}
}
}
All three methods are suspend functions, so call them from a coroutine. They return once the server has answered, and throw a VideoSDKException on failure. They also require a joined room. See Errors for what each method throws.
Errors
PubSub reports failures by throwing from the method (try / catch). Every failure except 3065 and 3073 also reaches RoomEventListener.onError(VideoSDKError). All three methods throw VideoSDKException. Use its code to tell the cases apart.
publish() throws:
3035(ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED) — The room has not been joined yet (or has already been left). Also firesonError.3065(ERROR_ROOM_RECONNECTING) — The room is reconnecting. Not reported ononError. The message is not sent later, so publish it again once the room isCONNECTED.4087(PUBSUB_PUBLISH_FAILED) — The message could not be sent, for example because the server rejected it, or the connection dropped before the server acknowledged it. Also firesonError. Publish it again once the room isCONNECTED.
subscribe() throws:
3073(ERROR_INVALID_PARAMETER) — "You already subscribed to this topic with same listener." One listener instance may hold at most one subscription per topic. Callunsubscribe()before re-subscribing, or create a second listener instance.3073(ERROR_INVALID_PARAMETER) —oldMessageLimitis negative. It must benull(all history) or>= 0.3073(ERROR_INVALID_PARAMETER) —maxQueueis less than1.3073(ERROR_INVALID_PARAMETER) —maxQueuewas set whilerealtimeOverflowisDROP. It is only valid withQUEUE.3073(ERROR_INVALID_PARAMETER) —newMessageLimitis negative. It must benullor>= 0.3035(ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED) — The room has not been joined yet (or has already been left). Also firesonError.3065(ERROR_ROOM_RECONNECTING) — The room is reconnecting. Not reported ononError.4088(PUBSUB_SUBSCRIBE_FAILED) — The subscription failed, for example because the server rejected it or the connection is down. Also firesonError. Subscribe again once the room isCONNECTED.
When subscribe() throws, or its coroutine is cancelled, nothing is registered. The 3073 errors are not reported on onError. Options are validated before the room state is checked, so an invalid configuration fails immediately.
unsubscribe() throws:
The topic itself is unsubscribed only when you remove its last listener, and only that call can throw. Removing any other listener, or one that is not subscribed to the topic, never throws.
3035(ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED) — The room has not been joined yet (or has already been left). Also firesonError.3065(ERROR_ROOM_RECONNECTING) — The room is reconnecting. Not reported ononError.4089(PUBSUB_UNSUBSCRIBE_FAILED) — The server call failed, for example because the connection is down. Also firesonError.
When unsubscribe() throws, the listener stays registered.
Applications of PubSub
Some features you can build with PubSub:
Chat: You can utilise this to develop various Chat features, such as Private Chat and Group Chat. You can follow our chat integration guide here.Raise Hand: You can allow attendees to raise their hands at any time in the room, informing everyone else that someone has done so.Layout Switching: You can change the room's layout for every participant at once, such as from Grid layout to Spotlight or from Grid Layout to Sidebar.Poll: You may make polls, let users respond to them, and display the results at the end of a poll.Question Answer Session: You can also design interactive functionality that is question-and-answer based.
Downloading PubSub Messages
All the messages from PubSub published with persist : true can be downloaded as a .csv file. This file will be available in the VideoSDK dashboard as well as through the Sessions API.
API Reference
The API references for all the methods and events utilised in this guide are provided below.
Got a Question? Ask us on discord

