Skip to content

Reference / cheat sheet

The one-page field card.

Ten sections of decisions, line items, close codes, stats, and failure probes. Dense on purpose: this is the page you keep open while the call is broken, not the page you read instead of the lessons.

10 sections/Print-friendly/No account
Read it in this orderFIELD USE
  1. 01

    Transport

    What shape of connection does the product need?

  2. 02

    Order

    Which stage of the connection is incomplete?

  3. 03

    Path

    Which candidate pair is selected, and at what cost?

  4. 04

    Measure

    What do the deltas say about the user's experience?

SAMPLE, COMPARE DELTAS, THEN NAME THE LAYER.

01

Before the architecture

Pick a transport

Start with direction, message rate, latency budget, and who is allowed to fan out. The protocol follows.

MechanismDirectionLatencyReach for it whenAvoid when
HTTP pollingRequest / responseSecondsSimple job status, maximum compatibilityYou need sub-second fan-out
Long pollingServer → client, heldHeldLegacy proxy constraints, low message rateMany idle clients or high message rate
SSEOne-way streamLowLive feeds and notifications, automatic reconnectClients must send low-latency messages
WebSocketFull duplexLowChat, signaling, shared state, frequent writesOne-way media at audience scale
WebRTC dataPeer-to-peer duplexLowestLow-latency arbitrary data and file transferCentral audit or fan-out is mandatory
WebRTC mediaPeer-to-peer or via SFULowestInteractive audio and videoHuge one-way audiences that tolerate delay

WebSocket is a transport for signaling and app data. WebRTC is a media/data transport. SFU is a topology.

Signal

Do not buy a media server to send JSON.

A WebSocket plus a room model handles most signaling and chat. Reach for WebRTC when the payload is real-time media or a peer-to-peer data path, not because the word “real-time” appears in the brief.

02

Transport / signaling

WebSocket field card

The handshake gets you a socket. Heartbeats, backpressure, close handling, and idempotency keep it useful.

Opcodes you will see

OpcodeMeaningPractical rule
0x0ContinuationA fragmented message is in progress
0x1TextUTF-8 JSON or text
0x2BinaryTyped arrays; set binaryType deliberately
0x8CloseRead code and reason; stop sending
0x9PingServer liveness probe; pong must follow
0xAPongApplication heartbeat acknowledgement

Browsers expose the WebSocket API, not raw frames. The frame layout is still the reason masking, fragmentation, and close codes exist.

Close codes

CodeNameWhen
1000Normal closureIntentional leave or completed work
1001Going awayServer restart, navigation, page unload
1002Protocol errorMalformed frame or handshake
1003Unsupported dataMessage kind the endpoint cannot accept
1008Policy violationAuth, rate limit, room policy
1009Message too bigFrame exceeds configured limit
1011Internal errorServer could not fulfill the request
4000–4999ApplicationUse for explicit product semantics

1005 and 1006 are reserved. They can be observed locally but must never be sent in a close frame.

Production checklist

Heartbeat
Ping every 20–30 seconds; close stale connections. Browser pong is automatic for protocol ping. Use an app-level ping if you need RTT telemetry.
Reconnect
Exponential backoff with jitter, a maximum delay, and a visible status. Rejoin the room and re-establish subscriptions after reconnect.
Idempotency
Give client messages IDs or sequence numbers. A reconnect can replay an unacknowledged send; the server must not apply it twice.
Backpressure
Check socket.bufferedAmount before sending bursts. Queue with a cap and drop or coalesce state updates instead of growing memory forever.
Validation
Enforce a maximum message size, a schema, a rate limit, and an origin/auth check at the server. The browser is not the authority.
Fan-out
One process is not a room service. Publish events across instances with Redis or NATS, and keep presence separate from message delivery.
03

The connection pipeline

WebRTC in order

When a call is stuck, identify the first stage that is incomplete. Do not debug media before you have a selected pair.

  1. 01

    Authorize and choose a room

    Authenticate the signaling socket, authorize the room, assign an anonymous peer identity, and enforce a participant cap.

  2. 02

    Capture media

    getUserMedia or getDisplayMedia must resolve in a secure context. Stop tracks when leaving; permission is not a connection.

  3. 03

    Create the peer connection

    Create RTCPeerConnection with STUN/TURN configuration. Add tracks or transceivers before creating the offer.

  4. 04

    Exchange SDP

    Offer describes media sections and DTLS/ICE parameters. Answer accepts or narrows them. Apply descriptions in the correct order.

  5. 05

    Trickle ICE candidates

    Send each candidate as it is gathered. Add remote candidates after the remote description; queue early candidates safely.

  6. 06

    Connectivity checks

    ICE tests candidate pairs. Both sides need compatible credentials and at least one working path. TURN relay is the fallback.

  7. 07

    DTLS handshake

    Validate certificates against the fingerprints in SDP, then derive keys. ICE can succeed while DTLS fails.

  8. 08

    SRTP media and SCTP data

    Encrypted RTP flows and data channels become usable. Watch inbound bytes—not just connection state—to confirm media.

  9. 09

    Renegotiate or restart

    New tracks need a new offer/answer. Network changes need createOffer({ iceRestart: true }), not a full page reload.

  10. 10

    Teardown

    Close data channels, stop tracks, close the peer connection, remove the participant, and release the signaling room.

The smallest useful skeleton

const pc = new RTCPeerConnection({ iceServers });

for (const track of localStream.getTracks()) {
  pc.addTrack(track, localStream);
}

pc.onicecandidate = ({ candidate }) => {
  if (candidate) signal.send({ type: "ice", candidate });
};

pc.ontrack = ({ streams }) => {
  remoteVideo.srcObject = streams[0];
};

await pc.setLocalDescription(await pc.createOffer());
signal.send({ type: "offer", sdp: pc.localDescription });

Signaling is your protocol. WebRTC does not define how offers, answers, or candidates reach the other peer.

Caution

State order is diagnostic.

ICE checking but never connected points to candidates, NAT, or TURN. ICE connected but the connection failed points to DTLS or fingerprint problems. Connection connected with no frames points to codecs, directions, or autoplay.

04

Session description

SDP lines to recognize

You do not need to memorize SDP. You need to know which line to inspect when negotiation or media fails.

LineWhat it controlsDebug signal
v=0SDP versionAlways zero in WebRTC
m=audio / m=video / m=applicationMedia sectionOrder must remain stable across renegotiation
a=mid:Section identityMissing or changed mids break bundling and renegotiation
a=ice-ufrag / a=ice-pwdICE credentialsMismatch means the pair cannot authenticate
a=fingerprintDTLS certificate hashMismatch fails after ICE succeeds
a=setup:actpassDTLS role offerOne side becomes active; both passive cannot handshake
a=rtpmap:Payload codecNo shared codec means no usable media
a=fmtp:Codec parametersProfiles and packetization details
a=sendrecv / sendonly / recvonly / inactiveDirectionTwo sendonly sections produce silence
a=candidate:ICE candidateInline only in non-trickle flows
a=group:BUNDLEBundled transportsAll sections share one transport tuple

SDP is capability negotiation, not a media stream. A valid SDP does not guarantee a network path or permission to play audio.

Renegotiation rules

Keep m-line order
Add media sections at the end. Reuse an existing transceiver instead of inventing a new one when removing and re-adding a track.
Use perfect negotiation
On glare, one polite peer rolls back and accepts the remote offer; the impolite peer ignores the collision. Never let both sides roll back.
Do not create a new PC for every track
Reuse the connection and renegotiate. A new peer connection loses ICE, DTLS, and data-channel state.
05

Find a path

ICE, STUN, TURN

Candidates are possibilities, pairs are tests, and the selected pair is the path media actually takes.

CandidateSourceUse it whenCost / risk
hostLocal interfaceSame LAN, direct tests, mDNS casesExposes a local address in SDP
srflxSTUN-discovered public mappingTypical NAT traversalSymmetric NAT can still block it
relayTURN allocationDirect paths fail or policy requires relayServer bandwidth and added latency
prflxPeer-reflexive discoveryA working path was discovered during checksNot gathered; observed from connectivity checks

Operational rules

Trickle by default
Do not wait for gathering complete unless the signaling protocol cannot carry candidates. Manual signaling in the 1:1 lab is the explicit exception.
Queue early candidates
If a candidate arrives before setRemoteDescription, queue it and flush after the description is applied.
Short-lived TURN credentials
Mint HMAC credentials on the server with a TTL. Never ship a permanent TURN username or secret in the client bundle.
Relay-only mode
Set iceTransportPolicy: "relay" to control IP exposure or test TURN. You must still configure a working TURN server.
ICE restart
Call createOffer({ iceRestart: true }) after a network change, then exchange a new offer and answer. A full reconnect is heavier.
Read the pair
getStats() selected candidate pair shows currentRoundTripTime, availableOutgoingBitrate, bytes sent/received, and relay type.

Context

TURN is a media relay, not a signaling service.

It forwards encrypted packets and cannot read SRTP content. Budget roughly two directions of media traffic per relayed participant, plus protocol overhead, and place relays close to users.

06

SCTP over DTLS

Data channel presets

The defaults are ordered and reliable. Override them deliberately for latency, and always design for backpressure.

UseOptionsWhy
Chatordered: true, reliableMessage order matters and loss must be repaired
Live cursor / pointerordered: false, maxRetransmits: 0Stale updates are worse than missing ones
Game state snapshotsordered: false, maxRetransmits: 0 or smallNewest state wins; never block on an old packet
File transferordered: true, reliableChunks must arrive complete and in order
Telemetry samplesordered: false, maxPacketLifeTime: smallAvoid head-of-line blocking; accept gaps

Backpressure-aware chunking

const CHUNK = 16 * 1024;
channel.bufferedAmountLowThreshold = 256 * 1024;

async function sendFile(file: File) {
  const buffer = await file.arrayBuffer();
  for (let offset = 0; offset < buffer.byteLength; offset += CHUNK) {
    if (channel.bufferedAmount > 512 * 1024) {
      await new Promise((resolve) => {
        channel.addEventListener("bufferedamountlow", resolve, { once: true });
      });
    }
    channel.send(buffer.slice(offset, offset + CHUNK));
  }
}

Without a buffer cap, a fast producer can exhaust memory long before the network drains.

Set binaryType
Use channel.binaryType = "arraybuffer" for binary payloads. Mixing strings and typed arrays without a schema is a debugging trap.
Frame your protocol
Include a message kind, a version, and a request or file ID. The data channel is a byte pipe, not a document database.
No server to rescue you
Peer-to-peer data channels have no central log. If audit, fan-out, or durability is required, use WebSocket or an application server.
07

Measurement

getStats numbers

Sample every couple of seconds, compute deltas, and compare the number to a user-visible symptom.

Compute a receive bitrate from deltas

let previous = null;

async function sample(pc) {
  const report = await pc.getStats();
  const now = performance.now();

  report.forEach((stat) => {
    if (stat.type === "inbound-rtp" && stat.kind === "video") {
      if (previous) {
        const seconds = (now - previous.at) / 1000;
        const kbps = ((stat.bytesReceived - previous.bytes) * 8) / seconds / 1000;
        console.log({ kbps, packetsLost: stat.packetsLost, jitter: stat.jitter });
      }
      previous = { at: now, bytes: stat.bytesReceived };
    }
  });
}

Counters are cumulative. A raw bytesReceived value is not a bitrate.

ReportFieldWhat to ask
inbound-rtpbytesReceivedIs media actually arriving?
inbound-rtppacketsLost / jitterIs the network damaging the call?
inbound-rtpframesPerSecond / framesDroppedIs decode healthy?
inbound-rtpqualityLimitationReasonIs the sender limited by CPU, bandwidth, or other?
outbound-rtpbytesSent / packetsSentIs the sender actually transmitting?
outbound-rtpqualityLimitationReasonIs the encoder the bottleneck?
candidate-paircurrentRoundTripTimeHow far away is the selected path?
candidate-pairavailableOutgoingBitrateWhat does congestion control think is available?
remote-inbound-rtpfractionLost / roundTripTimeWhat does the receiver report back?
data-channelbytesSent / bytesReceivedIs SCTP moving application data?
Sample, do not spam
Poll every 1–2 seconds and keep a short history. Calling getStats on every render is expensive and produces noise.
Quality limitation
cpu, bandwidth, and other are a first classification, not a root cause. Correlate with CPU usage, BWE, and selected pair.
Autoplay
Inbound bytes can be healthy while the user sees nothing. Check track unmuted, element playing, and browser autoplay policy.
Data channel
Track bufferedAmount on the sender and bytesReceived on the receiver. A stalled channel may be backpressure, not network loss.
08

Scale

Topology math

Count publishers separately from viewers. The same participant count can produce very different bills.

TopologyPeer connectionsPer-peer uploadServer mediaLatency
P2P11 streamNoneLowest
Meshn(n−1)/2(n−1) streamsNoneLowest for small rooms
SFUn1 stream (plus layers)Forwarded egressLow
MCUn1 streamMixed egress + high CPUHigher
Cascaded SFUsn per region + inter-SFU1 stream to local SFURegional + inter-regionLow if regions are close
Broadcastn subscribersStage onlyn × stream egressLow to HLS-like

For a full mesh with n equal publishers: directional streams = n(n − 1). Total client upload = n(n − 1)B. SFU egress can approach the same total when every viewer subscribes to every stream.

Quick decisions

Two people
P2P plus TURN. Do not run a media server to connect two browsers.
3–6 publishers with good devices
Full mesh can be acceptable. Enforce a hard cap and test weak uplinks.
7–50 interactive participants
SFU with simulcast, active-speaker subscription, and selective layers. Egress is the bill.
Constrained clients or server features
MCU or server-side processing. You are trading server CPU and latency for predictable client load.
Global interactive room
Cascaded SFUs. Forward only selected streams between regions.
One-way audience
Broadcast with WHIP/WHEP, HLS, or a CDN. Do not promise a mesh to 10,000 viewers.

Rule

Viewers are not publishers.

A 1,000-person webinar is not a 1,000-person mesh. Count the stage, the active speakers, and the subscribers separately before choosing a topology or estimating egress.

09

Operations

Debug in order

Fix the earliest broken stage. A media symptom cannot tell you which layer failed until you inspect the sequence.

  1. 01

    Secure context and permission

    HTTPS or localhost, Permissions-Policy allows camera/mic, user granted access, and the correct device is selected.

  2. 02

    Local tracks

    Check getTracks() kinds, readyState, enabled, and muted. A black preview can be a disabled track or an unattached element.

  3. 03

    Signaling

    Socket open, join acknowledged, room present, offer and answer applied in the right order, target peer still present.

  4. 04

    ICE

    Candidates gathered, remote candidates added, candidate pair selected, TURN reachable. Read iceConnectionState and getStats pairs.

  5. 05

    DTLS and media

    Fingerprints match, DTLS connected, tracks received, element playing, inbound bytes increasing, autoplay policy satisfied.

  6. 06

    Quality

    RTT, loss, jitter, bitrate, frames dropped, qualityLimitationReason, CPU. Compare sender and receiver views.

  7. 07

    Teardown

    Tracks stopped, channels closed, peer connections closed, room membership removed, object URLs revoked.

SymptomFirst probeLikely layer
No permission promptSecure context, Permissions-Policy, browser settingsMedia capture
Camera preview works, remote is blackontrack, autoplay, inbound bytes, track mutedPlayback / autoplay
ICE stuck at checkingCandidate exchange, STUN/TURN, selected pairNetwork / signaling
ICE connected, connection failedDTLS fingerprint, a=setup, transport stateDTLS
Audio is roboticpacketsLost, jitter, bitrate, CPUNetwork / codec
Data channel open, file stallsbufferedAmount, bufferedamountlow, chunk sizeBackpressure / SCTP
Works on localhost, fails on phoneHTTPS, LAN address, TURN, firewallSecure context / network
Signaling dies, media freezesPeer connection state, ICE state, selected pairNetwork path / lifecycle

Use browser WebRTC internals for a chronological event log. Use getStats for the current numeric truth. Use server logs for signaling state.

10

Privacy

Security boundaries

Encryption protects a hop. Authorization, signaling trust, and media-server visibility are separate design decisions.

DTLS-SRTP
Encrypts media in transit between transport endpoints. Fingerprints in SDP bind the signaling exchange to the keys.
SFU visibility
A normal SFU terminates DTLS and can observe media. End-to-end encryption requires a separate design such as insertable streams or SFrame.
TURN credentials
Mint short-lived credentials server-side. A permanent secret in the browser is a public relay for anyone who opens devtools.
IP exposure
ICE candidates can reveal network addresses. Use relay-only policy when that is part of the threat model, and accept the bandwidth cost.
Signaling trust
SDP and candidates are control data. Authenticate room membership, validate message shapes, rate-limit, and never trust the client's claimed identity.
Permissions
Request the minimum tracks, stop them on leave, and do not keep a camera alive in a hidden tab. Camera and mic prompts are a product surface.

Caution

Do not log the whole session as a convenience.

SDP can contain addresses and fingerprints; signaling logs can reveal room membership and behavior. Redact candidates and peer identifiers in production telemetry, and retain only what operations actually needs.

The shortest path from this card to the certificate is still the labs. Open a real path and break it.