Connection patterns·续

   {
     "tracks": [
       { "location": "remote", "sessionId": "<MEDIA_PUBLISHER_SESSION_ID>", "trackName": "camera" }
     ]
   }
   ```

Map successful receiving `mid` values to their publications. Use `event.transceiver.mid` in the `track` handler to attach each received track to its playback element.
3. **Finish media negotiation.** Await `pc.setRemoteDescription(response.sessionDescription)`, `pc.createAnswer()`, and `pc.setLocalDescription(answer)`. After candidate gathering, send `pc.localDescription` through your backend to `PUT /sessions/{subscriberSessionId}/renegotiate`:

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

Wait for the API to accept it and confirm media playback before continuing.
4. **Add DataChannel transport.** Keep this same PeerConnection and session. Call `POST /sessions/{subscriberSessionId}/datachannels/establish` once:

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

Retain the successful allocation and reserved `server-events` channel for cleanup.
5. **Finish the new negotiation.** Apply this new SFU offer with `pc.setRemoteDescription()`. Create and set a new local answer. After candidate gathering, submit `pc.localDescription` through your backend to `PUT /sessions/{subscriberSessionId}/renegotiate`:

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

Await each browser operation and the API's acceptance of the answer. Wait for the PeerConnection to be `connected` before allocating the control subscription.
6. **Subscribe with reply permission.** Call `POST /sessions/{subscriberSessionId}/datachannels/new`:

```json
   {
     "dataChannels": [
       {
         "location": "remote",
         "sessionId": "<CONTROL_PUBLISHER_SESSION_ID>",
         "dataChannelName": "controls",
         "canReply": true
       }
     ]
   }
   ```

Check the result. At most one subscriber can hold reply access for this publisher channel. Granting access to this subscriber replaces any previous holder. Your backend controls who receives that permission.
7. **Open and verify controls.** Return the subscription's `id` to the browser as `channelId`:

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

Wait for `open`. Send a command defined by your application and confirm that the publisher receives it while media continues playing.

The [cloud-gaming guide](https://developers.cloudflare.com/realtime/sfu/examples/cloud-gaming/) shows media and input on one browser connection, including controller ownership and separate delivery settings for discrete and replaceable input.

## Add media to an existing connection

Start with a connected PeerConnection and keep its SFU session. Serialize additions with [other operations on that session](https://developers.cloudflare.com/realtime/sfu/concepts/negotiation/#serialize-mutations-per-session).

- **Publish more media:** Add transceivers for the new tracks, then create and set a fresh local offer. Follow the [publication recipe](#publish-audio-or-video) from `tracks/new`, sending the new local entries with the gathered `pc.localDescription`.
- **Receive more media:** Call `tracks/new` on the same session with additional remote entries in the [receive request format](#receive-a-published-track). [Complete any returned SFU offer](#complete-an-sfu-offer).

Finish the required SDP exchange before the next mutation.

## Add DataChannels to a media connection

Start here when an existing PeerConnection has completed media-only negotiation and has no DataChannel transport. Keep its SFU session and finish any outstanding operation first.

1. Install a `datachannel` listener to retain the reserved channel for teardown. Call `POST /sessions/{sessionId}/datachannels/establish` with `dataChannel: { location: "remote", dataChannelName: "server-events" }`.
2. Check the allocation and apply the returned offer. Await creation and local application of a new answer. After candidate gathering, submit `pc.localDescription` through your backend to `PUT /sessions/{sessionId}/renegotiate` as `sessionDescription`.
3. Wait for API acceptance and a `connected` PeerConnection. You can now [add application channels](https://developers.cloudflare.com/realtime/sfu/features/datachannels/#add-an-application-channel) using `datachannels/new`.

## Other initial negotiation sequences

The recipes use a known starting state. Existing integrations may negotiate transport in a different order:

- **DataChannels before media:** complete the message recipe's transport exchange, then add media on the same session. Finish each exchange before the next operation.
- **Media and DataChannels in the first offer:** an endpoint can negotiate both in its initial `tracks/new` offer. After that exchange, skip `datachannels/establish` and allocate application channels through `datachannels/new`. The [Pocket Radio firmware walkthrough ↗︎](https://github.com/cloudflare/realtime-examples/blob/main/esp32-radio/firmware/docs/sfu.md) demonstrates this setup.
- **DataChannel setup already started:** finish its outstanding offer/answer exchange and connection wait. An already-negotiated transport can carry additional application channels. Do not use `datachannels/establish` as a repeated initialization check.

## Complete an SFU offer

An operation returning `sessionDescription.type: "offer"` with `requiresImmediateRenegotiation: true` needs an answer before the next mutation on that session. The recipes include this exchange. Use this reference when implementing another operation.

1. In the browser, await `pc.setRemoteDescription(response.sessionDescription)`.
2. Await `pc.createAnswer()` and `pc.setLocalDescription(answer)`. After candidate gathering, return `pc.localDescription` to your backend.
3. Send `PUT /sessions/{sessionId}/renegotiate` with `sessionDescription: { type: "answer", sdp: "<LOCAL_ANSWER_SDP>" }`.
4. Check that the API accepted the answer before starting another mutation. A browser's `signalingState` becoming `stable` does not confirm that the backend has submitted the answer.

## Close media tracks

Use the `mid` from the connection you are modifying. Closing a receiving track stops that subscription. Closing a publishing track stops the source. A successful close of a bidirectional transceiver stops both directions. Subscribers' PeerConnections can remain connected, and media already buffered can still play.

### Close with negotiation

Start with an established connection and no unfinished offer/answer exchange.

1. **Prepare the endpoint's offer.** In the browser, save the `mid` of each transceiver to close, then call its `stop()` method. Create an offer with `await pc.createOffer()` and apply it with `await pc.setLocalDescription(offer)`.
2. **Close the tracks.** After candidate gathering, send `PUT /sessions/{sessionId}/tracks/close` from your backend with those mids and the browser's `pc.localDescription` as `sessionDescription`:

```json
   {
     "tracks": [{ "mid": "<TRANSCEIVER_MID>" }],
     "sessionDescription": { "type": "offer", "sdp": "<LOCAL_SDP>" },
     "force": false
   }
   ```

3. **Apply the answer.** Check request-level and per-track errors. In the browser, await `pc.setRemoteDescription(response.sessionDescription)` with the returned answer, even if individual tracks failed. Other tracks may have closed.
4. **Handle unfinished work.** Complete this exchange before another mutation. Retain failed closures for retry. If no usable answer arrives, follow [recovery guidance](https://developers.cloudflare.com/realtime/sfu/concepts/negotiation/#retry-and-reconnect).

Use [close results](https://developers.cloudflare.com/realtime/sfu/observability/error-codes/#interpret-close-results) to distinguish successful closure, an already-closed track, and an unresolved failure.

### Stop forwarding without negotiation

Set `force: true` to close selected media flows without exchanging SDP. This is useful when the endpoint cannot supply a close offer. On the backend, send `PUT /sessions/{sessionId}/tracks/close`:

```json
{
  "tracks": [{ "mid": "<TRANSCEIVER_MID>" }],
  "force": true
}

Check each track result. No /renegotiate call is needed. Forced closure can leave records in session information. Follow teardown ordering for outstanding negotiation or requests.

Clean up

Stop accepting new operations and withdraw ended publications from discovery. Close allocated media tracks and DataChannels, then close the endpoint's PeerConnection and stop captures when no longer needed. Follow cleanup responsibilities if the endpoint leaves before finishing.

Was this helpful?

YesNo

On this page

Connection patterns·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/realtime/sfu/get-started/connection-patterns/#page","headline":"Connection patterns","description":"Follow browser and backend recipes to publish media, receive tracks, exchange messages, or receive media and send controls.","url":"https://developers.cloudflare.com/realtime/sfu/get-started/connection-patterns/","inLanguage":"en","image":"https://developers.cloudflare.com/realtime/sfu/get-started/connection-patterns/og.png?v=627d452af49868dc","dateModified":"2026-09-22","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: Diagnose Realtime SFU setup, negotiation, media, DataChannel, and adapter problems using endpoint and backend observations. title: Observability image: https://developers.cloudflare.com/realtime/sfu/observability/og.png?v=533816c5c07b4fa8


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/realtime/llms.txt
Use this file to discover all available pages before exploring further.