Data Channel
Ordered/reliable semantics, topic reuse, size limits, messaging and the boundary with IM.
A data channel is "reliable messaging over the RTC connection": a good fit for whiteboard deltas, courseware commands, cursor positions and simple signalling; it is not suitable for chat bubbles, offline messages or message history — that is IM's job (this project does not provide IM; see the integration boundary in the RTC provider interface definition).
What each platform can do (decide before you choose)
| Use case | Data channel | IM |
|---|---|---|
| Cursor position / whiteboard deltas (high frequency, lossy is acceptable) | ✅ | ❌ Too slow |
| Raise hand, mute commands (low frequency, must be reliable) | ✅ | ✅ |
| Chat messages / offline reachability | ❌ Not guaranteed | ✅ |
| Large file transfer | ❌ Not suitable | ✅ Use object storage |
Data channels are bound to the lifetime of the channel connection: when the connection drops the channel is gone, and nothing is resent.
Web usage (signatures from the public interface definition)
import AgoraRTC from '@nimeet/rtc-web';
const client = AgoraRTC.createClient({ mode: 'rtc' });
// 1) Create and publish a data channel: the publish overload accepts IDataChannelConfig
const dataChannel = await client.publish({ ordered: true }); // ILocalDataChannel
// 2) Send: a binary payload (Uint8Array), not an arbitrary JS object
await dataChannel.send(new Uint8Array([1, 2, 3]));
// 3) Receive: remote channels are delivered through stream-message
client.on('stream-message', (uid, payload) => {
console.log('from', uid, payload.byteLength);
});
// 4) Remember to unpublish when you are done
await client.unpublish(dataChannel);Key points:
publish(config)returns anILocalDataChannel, andunpublish(dataChannel)takes it back;- Payloads are binary: encode and decode JSON yourself with
TextEncoder/TextDecoder; - Channel parameters (ordered, maximum retransmissions) live in
IDataChannelConfigand can be read back after publishing throughdataChannel.getConfig()(IDataChannelexposes read-only properties such asid,maxRetransmitsandmetadata).
Encoding convention (used consistently in our team)
const encoder = new TextEncoder();
const decoder = new TextDecoder();
function sendJson(channel, value) {
return channel.send(encoder.encode(JSON.stringify(value)));
}
client.on('stream-message', (uid, payload) => {
const message = JSON.parse(decoder.decode(payload));
// { t: 'cursor', x, y } / { t: 'whiteboard.op', op }
});Adding a t (type) field for multiplexing makes compatibility handling and instrumentation easier than opening several channels.
Size and rate
| Item | Recommendation | Rationale |
|---|---|---|
| Single payload | ≤ 1 KiB normally, ≤ 16 KiB at most | Beyond a UDP-friendly size, fragmentation and retransmission become likely |
| Send rate | ≤ 30 messages/second | Merge higher-frequency events on the client first |
| Reliability | Ordered and reliable for commands; cursors may drop to unreliable | ordered / maxRetransmits in IDataChannelConfig |
Native platforms
On native platforms the equivalent surface is the sendStreamMessage(streamId, data, length) / createDataStream(config) pair (check the API reference for each platform to see whether a method exists), with the same semantics as the web: create the stream first, then send messages, and dispatch incoming messages by streamId in the callback.
Common issues
| Symptom | Cause | Handling |
|---|---|---|
| No messages received | Only audio and video were subscribed, stream-message was not handled | Make sure the event is registered |
| Sending throws | The channel was already unpublished or the connection dropped | Recreate the channel in connection-state-change |
| Received data is garbled | The string was passed as the payload directly | Use TextEncoder/TextDecoder consistently |
| Duplicated messages | A retransmitting configuration was used | De-duplicate by message id in the application |
| Large messages are lost | Above the implementation limit | Fragment them and add application-level sequence numbers |