Connection patterns

Last updated Sep 22, 2026 Choose the task for your endpoint. Each initial setup recipe starts with a new browser connection and matching SFU session. Your application can combine these endpoints into rooms, broadcasts, and device interfaces.

Create the SFU session when ready to begin that connection's SDP exchange. Unconnected sessions can expire before their first operation.

Task Result Runnable example
Publish audio or video Make local media available to subscribers Video room
Receive a published track Play another endpoint's media Video room
Send messages between two endpoints Exchange application data without media tracks DataChannel example ↗︎
Receive media and send controls Watch a publisher and return authorized input Cloud gaming

For an existing connection, refer to Add media, Add DataChannels, or Close media tracks. To run an application before implementing these exchanges, follow the quickstart.

Before you start

Create an SFU app. Your backend holds its App Secret and makes every SFU API request shown here. Use Connection API authentication. Paths are relative to https://rtc.live.cloudflare.com/v1/apps/{appId}.

In a browser, an RTCPeerConnection manages one connection to the SFU. The matching sessionId identifies that connection in API calls. Session Description Protocol (SDP) describes the connection's media and transport settings. The browser and SFU exchange an offer and answer to agree on those settings.

Check the HTTP status, public errors, and individual resource results on every API call. Retain successful allocations for cleanup. Complete each request and its required SDP exchange before starting the next mutation on the same session. Different sessions can proceed independently. Negotiation and session lifecycle explains how to coordinate overlapping handlers.

Network connectivity

WebRTC uses Interactive Connectivity Establishment (ICE) to find a working network path. Configure it for the networks your endpoints support. Cloudflare's public STUN server is stun.cloudflare.com:3478. STUN helps discover network addresses. TURN can relay traffic when an endpoint's network requires it.

Install connection-state handlers and give connection waits a timeout that reports failure. After setting a local description, gather network candidates before sending pc.localDescription to your backend. The cloud-gaming WebRTC helpers ↗︎ demonstrate bounded candidate gathering.

Publish audio or video

This recipe publishes a browser camera or microphone. It creates a media connection without DataChannel transport.

  1. Prepare the media offer. In the browser, capture tracks with navigator.mediaDevices.getUserMedia(). Create an RTCPeerConnection, called pc here, and add each track with pc.addTransceiver(track, { direction: "sendonly" }). Create an offer with await pc.createOffer(), then apply it with await pc.setLocalDescription(offer).

Wait for candidate gathering, then read each transceiver's mid. This identifier associates a media section in this connection's SDP with the track you publish. 2. Create the SFU session. On your backend, call POST /sessions/new with no request body. Store the returned sessionId with this connection. 3. Publish the tracks. On the backend, call POST /sessions/{sessionId}/tracks/new with the browser's pc.localDescription as sessionDescription:

json { "sessionDescription": { "type": "offer", "sdp": "<LOCAL_SDP>" }, "tracks": [ { "location": "local", "mid": "<TRANSCEIVER_MID>", "trackName": "camera" } ] }

Replace the placeholders with the generated SDP and assigned mid. To publish camera and microphone together, include both tracks in this request. Each entry uses its own mid and trackName. Refer to batch resource operations. 4. Apply the answer. In the browser, await pc.setRemoteDescription(response.sessionDescription) with the returned SFU answer. This completes the offer/answer exchange. No /renegotiate call is needed for this answer. 5. Make the publication discoverable. Wait for the connection to become connected. Share successful publications' session ID and track names with authorized subscribers through your application.

The publication is ready for another endpoint to request. Use Receive a published track to verify playback.

Receive a published track

Start with the publisher's session ID and track name, obtained through your application. This receiving-only browser does not need camera or microphone access.

  1. Create the receiving connection. Create a new RTCPeerConnection, called pc, and install a track event handler. On the backend, call POST /sessions/new with no request body. Save its ID as subscriberSessionId.
  2. Request the publication. On the backend, call POST /sessions/{subscriberSessionId}/tracks/new without a local offer:

json { "tracks": [ { "location": "remote", "sessionId": "<PUBLISHER_SESSION_ID>", "trackName": "camera" } ] }

To receive several publications together, include one entry for each in tracks. They can come from different publishers. This request adds them all to the same receiving session. 3. Answer the SFU offer. Map each successful result's receiving mid to its publication before applying the returned offer. In the browser, await pc.setRemoteDescription(response.sessionDescription), then pc.createAnswer() and pc.setLocalDescription(answer).

In the track handler, use event.transceiver.mid to select the publication's playback element. For one received track, set element.srcObject = new MediaStream([event.track]). 4. Finish the exchange. After candidate gathering, return pc.localDescription to your backend. Send it as sessionDescription to PUT /sessions/{subscriberSessionId}/renegotiate:

json { "sessionDescription": { "type": "answer", "sdp": "<LOCAL_ANSWER_SDP>" } }

Continue only after the API accepts the answer. 5. Verify reception. Wait for the connection to become connected and confirm that the requested media plays. Use media troubleshooting when the connection succeeds but playback does not.

Send messages between two endpoints

This recipe connects a publisher and a subscriber without audio or video. It uses reliable, ordered delivery. First connect each endpoint's DataChannel transport, then allocate the application channel carried over it.

  1. Create both connections. In each browser, create a new RTCPeerConnection. Install a datachannel event listener to retain channels opened by the SFU and close them during teardown. On the backend, call POST /sessions/new with no body for each connection. Save publisherSessionId and subscriberSessionId.
  2. Connect DataChannel transport. For each session, call POST /sessions/{sessionId}/datachannels/establish once:

json { "dataChannel": { "location": "remote", "dataChannelName": "server-events" } }

Check the returned dataChannel allocation. The server-events channel is reserved for transport setup. The application channel is allocated separately. 3. Answer each SFU offer. In the corresponding browser, await pc.setRemoteDescription(response.sessionDescription), create an answer, and await pc.setLocalDescription(answer). After candidate gathering, return pc.localDescription to your backend and send PUT /sessions/{sessionId}/renegotiate:

json { "sessionDescription": { "type": "answer", "sdp": "<THIS_ENDPOINT_ANSWER_SDP>" } }

Wait for each answer to be accepted and both PeerConnections to become connected before allocating application channels. 4. Publish the application channel. On the backend, call POST /sessions/{publisherSessionId}/datachannels/new:

json { "dataChannels": [{ "location": "local", "dataChannelName": "messages" }] }

Retain the successful publisher allocation and its id. 5. Subscribe to the channel. Call POST /sessions/{subscriberSessionId}/datachannels/new:

json { "dataChannels": [ { "location": "remote", "sessionId": "<PUBLISHER_SESSION_ID>", "dataChannelName": "messages" } ] }

Retain the successful subscriber allocation and its id. 6. Create the browser channels. Return each allocation's id to its own endpoint as channelId. On each PeerConnection, create the channel with that ID:

js const channel = pc.createDataChannel("messages", { negotiated: true, id: channelId, });

Publisher and subscriber IDs can differ. This browser configuration matches the reliable, ordered API defaults. Install a message handler on the subscriber and wait for both channels to open. 7. Send and verify. After the subscriber confirms it is ready through your application, call channel.send("hello") on the publisher. Confirm the subscriber's message event contains hello.

You now have publisher-to-subscriber messages. To add a return path, use DataChannel replies. For another delivery policy, publish a separate named channel and have its subscribers mirror that policy. Refer to delivery settings.

Receive media and send controls

This recipe uses one browser connection for incoming media and outgoing controls. The publisher already supplies a media track and a reliable, ordered controls DataChannel. Your backend has both publication locators and has authorized this browser as the controller.

  1. Create the receiving connection. Create a new RTCPeerConnection, called pc. Install track and datachannel listeners. On the backend, call POST /sessions/new with no body and save subscriberSessionId. Do not create browser DataChannels during this first media exchange.
  2. Request media. Call POST /sessions/{subscriberSessionId}/tracks/new without a local offer: