DataChannels are a way to send arbitrary data, not just audio or video data, between client in low latency. DataChannels are useful for scenarios like chat, game state, or any other data that doesn't need to be encoded as audio or video but still needs to be sent between clients in real time.
While it is possible to send audio and video over DataChannels, it's not optimal because audio and video transfer includes media specific optimizations that DataChannels do not have, such as simulcast, forward error correction, better caching across the Cloudflare network for retransmissions.
graph LR
A[Publisher] -->|Arbitrary data| B[Cloudflare Realtime SFU]
B -->|Arbitrary data| C@{ shape: procs, label: "Subscribers"}
DataChannels on Cloudflare Realtime can scale up to many subscribers per publisher, there is no limit to the number of subscribers per publisher.
- Create two Realtime sessions, one for the publisher and one for the subscribers.
- Create a DataChannel by calling /datachannels/new with the location set to "local" and the dataChannelName set to the name of the DataChannel.
- Create a DataChannel by calling /datachannels/new with the location set to "remote" and the sessionId set to the sessionId of the publisher.
- Use the DataChannel to send data from the publisher to the subscribers.
By default, a subscriber starts receiving messages as soon as it pulls a remote DataChannel. To avoid losing the first messages before your subscriber is ready to handle them, you can opt in to an acknowledgment gate for each subscriber.
- Set
waitForAck: truewhen you create a remote DataChannel with the HTTPS API. While the gate is closed, the SFU does not forward any messages to that subscriber on that DataChannel. - After the subscriber's DataChannel opens, have the subscriber send any message on it (for example, the string
"ack"). The first message opens the gate, and the SFU starts forwarding messages to that subscriber. - The acknowledgment is consumed by the SFU. It is not forwarded to the publisher or to other subscribers. Without
canReply, the channel stays publisher-to-subscriber only. - The acknowledgment must reach the SFU within 15 seconds of creating the remote DataChannel. If it does not, the SFU tears down the gated channel and forwards no messages; create the remote DataChannel again to retry.
waitForAckapplies only tolocation: "remote"DataChannels and defaults tofalse, so existing behavior is unchanged.
Create a remote DataChannel with the gate enabled by calling POST /apps/{appId}/sessions/{sessionId}/datachannels/new on the subscriber session:
{
"dataChannels": [
{
"location": "remote",
"sessionId": "<publisherSessionId>",
"dataChannelName": "my-channel",
"waitForAck": true
}
]
}Then, on the subscriber, send the acknowledgment once the DataChannel is open:
const resp = await fetch(
`${API_BASE}/sessions/${subscriberId}/datachannels/new`,
{
method: "POST",
headers,
body: JSON.stringify({
dataChannels: [
{
location: "remote",
sessionId: publisherId,
dataChannelName: "my-channel",
waitForAck: true,
},
],
}),
},
).then((r) => r.json());
const dc = pc.createDataChannel("my-channel-subscribed", {
negotiated: true,
id: resp.dataChannels[0].id,
});
await waitForOpen(dc);
dc.send("ack"); // The first frame opens the gate; later frames are your application data.By default, Cloudflare Realtime SFU DataChannels are one-way. The publisher sends, and every subscriber that pulled the channel receives. Subscribers cannot send data back to the publisher unless you opt in with canReply.
canReply lets one subscriber send messages back to the publisher on the same remote DataChannel it pulled. Forward traffic (publisher to subscribers) does not change. The SFU admits reverse traffic only from the subscriber that holds canReply, and delivers those messages only to the publisher, not to other subscribers.
graph LR
P[Publisher] -->|forward| SFU[Cloudflare Realtime SFU]
SFU -->|forward| S1[Subscriber with canReply]
SFU -->|forward| S2[Other subscribers]
S1 -->|reverse| SFU
SFU -->|reverse| P
canReply follows these rules:
- Set
canReply: truewhen the subscriber creates a remote DataChannel withPOST .../datachannels/new. You can also grant or revoke it later withPUT .../datachannels/updateon that subscriber session. canReplyapplies only tolocation: "remote"DataChannels and defaults tofalse, so existing pull behavior does not change.- At most one subscriber may hold
canReplyfor a given publisher DataChannel at a time. GrantingcanReplyto another subscriber replaces the previous holder. To revoke, setcanReply: falseon the subscriber that currently holds it. - The SFU does not forward messages from a non-admitted subscriber to the publisher.
- The SFU does not fan out reverse messages to other subscribers.
Create the remote DataChannel on the subscriber session with canReply: true:
{
"dataChannels": [
{
"location": "remote",
"sessionId": "<publisherSessionId>",
"dataChannelName": "my-channel",
"canReply": true
}
]
}Example flow:
- Publisher creates a local DataChannel named
my-channel. - Subscriber pulls it with
canReply: trueand opens the negotiated channel in the browser. - Publisher sends on the channel; the subscriber receives (forward path).
- Subscriber sends on the same channel; the publisher receives (reverse path).
To change admission without recreating the pull, call PUT /apps/{appId}/sessions/{subscriberSessionId}/datachannels/update:
{
"dataChannels": [
{
"location": "remote",
"sessionId": "<publisherSessionId>",
"dataChannelName": "my-channel",
"canReply": true
}
]
}Use the same body with "canReply": false to revoke. The following table lists common patterns:
| Goal | Action |
|---|---|
| Pull without reverse, then allow reverse | Create remote without canReply (or canReply: false). Confirm reverse is blocked. Call update with canReply: true. |
| Move reverse rights to another subscriber | On the new subscriber, call update with canReply: true. The previous holder loses reverse admission. |
| Stop reverse traffic | On the current holder, call update with canReply: false. |
// Subscriber already pulled "my-channel" without canReply.
// Grant reverse admission later:
await fetch(`${API_BASE}/sessions/${subscriberId}/datachannels/update`, {
method: "PUT",
headers,
body: JSON.stringify({
dataChannels: [
{
location: "remote",
sessionId: publisherId,
dataChannelName: "my-channel",
canReply: true,
},
],
}),
});
// Same negotiated DataChannel instance; subscriber can now send and the publisher receives.
dc.send(JSON.stringify({ type: "reply", body: "pong" }));You can set both canReply and waitForAck on the same remote DataChannel. waitForAck still gates forward delivery to that subscriber until it sends its first message. That first message is consumed as the acknowledgment and is not forwarded. After the gate opens, further subscriber messages on an admitted canReply channel become reverse traffic to the publisher.
An example of DataChannels in action can be found in the Realtime Examples github repo ↗.