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.
- 01
Transport
What shape of connection does the product need?
- 02
Order
Which stage of the connection is incomplete?
- 03
Path
Which candidate pair is selected, and at what cost?
- 04
Measure
What do the deltas say about the user's experience?
SAMPLE, COMPARE DELTAS, THEN NAME THE LAYER.
Before the architecture
Pick a transport
Start with direction, message rate, latency budget, and who is allowed to fan out. The protocol follows.
| Mechanism | Direction | Latency | Reach for it when | Avoid when |
|---|---|---|---|---|
| HTTP polling | Request / response | Seconds | Simple job status, maximum compatibility | You need sub-second fan-out |
| Long polling | Server → client, held | Held | Legacy proxy constraints, low message rate | Many idle clients or high message rate |
| SSE | One-way stream | Low | Live feeds and notifications, automatic reconnect | Clients must send low-latency messages |
| WebSocket | Full duplex | Low | Chat, signaling, shared state, frequent writes | One-way media at audience scale |
| WebRTC data | Peer-to-peer duplex | Lowest | Low-latency arbitrary data and file transfer | Central audit or fan-out is mandatory |
| WebRTC media | Peer-to-peer or via SFU | Lowest | Interactive audio and video | Huge 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.
Transport / signaling
WebSocket field card
The handshake gets you a socket. Heartbeats, backpressure, close handling, and idempotency keep it useful.
Opcodes you will see
| Opcode | Meaning | Practical rule |
|---|---|---|
| 0x0 | Continuation | A fragmented message is in progress |
| 0x1 | Text | UTF-8 JSON or text |
| 0x2 | Binary | Typed arrays; set binaryType deliberately |
| 0x8 | Close | Read code and reason; stop sending |
| 0x9 | Ping | Server liveness probe; pong must follow |
| 0xA | Pong | Application 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
| Code | Name | When |
|---|---|---|
| 1000 | Normal closure | Intentional leave or completed work |
| 1001 | Going away | Server restart, navigation, page unload |
| 1002 | Protocol error | Malformed frame or handshake |
| 1003 | Unsupported data | Message kind the endpoint cannot accept |
| 1008 | Policy violation | Auth, rate limit, room policy |
| 1009 | Message too big | Frame exceeds configured limit |
| 1011 | Internal error | Server could not fulfill the request |
| 4000–4999 | Application | Use 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.
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.
- 01
Authorize and choose a room
Authenticate the signaling socket, authorize the room, assign an anonymous peer identity, and enforce a participant cap.
- 02
Capture media
getUserMedia or getDisplayMedia must resolve in a secure context. Stop tracks when leaving; permission is not a connection.
- 03
Create the peer connection
Create RTCPeerConnection with STUN/TURN configuration. Add tracks or transceivers before creating the offer.
- 04
Exchange SDP
Offer describes media sections and DTLS/ICE parameters. Answer accepts or narrows them. Apply descriptions in the correct order.
- 05
Trickle ICE candidates
Send each candidate as it is gathered. Add remote candidates after the remote description; queue early candidates safely.
- 06
Connectivity checks
ICE tests candidate pairs. Both sides need compatible credentials and at least one working path. TURN relay is the fallback.
- 07
DTLS handshake
Validate certificates against the fingerprints in SDP, then derive keys. ICE can succeed while DTLS fails.
- 08
SRTP media and SCTP data
Encrypted RTP flows and data channels become usable. Watch inbound bytes—not just connection state—to confirm media.
- 09
Renegotiate or restart
New tracks need a new offer/answer. Network changes need createOffer({ iceRestart: true }), not a full page reload.
- 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.
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.
| Line | What it controls | Debug signal |
|---|---|---|
| v=0 | SDP version | Always zero in WebRTC |
| m=audio / m=video / m=application | Media section | Order must remain stable across renegotiation |
| a=mid: | Section identity | Missing or changed mids break bundling and renegotiation |
| a=ice-ufrag / a=ice-pwd | ICE credentials | Mismatch means the pair cannot authenticate |
| a=fingerprint | DTLS certificate hash | Mismatch fails after ICE succeeds |
| a=setup:actpass | DTLS role offer | One side becomes active; both passive cannot handshake |
| a=rtpmap: | Payload codec | No shared codec means no usable media |
| a=fmtp: | Codec parameters | Profiles and packetization details |
| a=sendrecv / sendonly / recvonly / inactive | Direction | Two sendonly sections produce silence |
| a=candidate: | ICE candidate | Inline only in non-trickle flows |
| a=group:BUNDLE | Bundled transports | All 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.
Find a path
ICE, STUN, TURN
Candidates are possibilities, pairs are tests, and the selected pair is the path media actually takes.
| Candidate | Source | Use it when | Cost / risk |
|---|---|---|---|
| host | Local interface | Same LAN, direct tests, mDNS cases | Exposes a local address in SDP |
| srflx | STUN-discovered public mapping | Typical NAT traversal | Symmetric NAT can still block it |
| relay | TURN allocation | Direct paths fail or policy requires relay | Server bandwidth and added latency |
| prflx | Peer-reflexive discovery | A working path was discovered during checks | Not 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.
SCTP over DTLS
Data channel presets
The defaults are ordered and reliable. Override them deliberately for latency, and always design for backpressure.
| Use | Options | Why |
|---|---|---|
| Chat | ordered: true, reliable | Message order matters and loss must be repaired |
| Live cursor / pointer | ordered: false, maxRetransmits: 0 | Stale updates are worse than missing ones |
| Game state snapshots | ordered: false, maxRetransmits: 0 or small | Newest state wins; never block on an old packet |
| File transfer | ordered: true, reliable | Chunks must arrive complete and in order |
| Telemetry samples | ordered: false, maxPacketLifeTime: small | Avoid 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.
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.
| Report | Field | What to ask |
|---|---|---|
| inbound-rtp | bytesReceived | Is media actually arriving? |
| inbound-rtp | packetsLost / jitter | Is the network damaging the call? |
| inbound-rtp | framesPerSecond / framesDropped | Is decode healthy? |
| inbound-rtp | qualityLimitationReason | Is the sender limited by CPU, bandwidth, or other? |
| outbound-rtp | bytesSent / packetsSent | Is the sender actually transmitting? |
| outbound-rtp | qualityLimitationReason | Is the encoder the bottleneck? |
| candidate-pair | currentRoundTripTime | How far away is the selected path? |
| candidate-pair | availableOutgoingBitrate | What does congestion control think is available? |
| remote-inbound-rtp | fractionLost / roundTripTime | What does the receiver report back? |
| data-channel | bytesSent / bytesReceived | Is 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.
Scale
Topology math
Count publishers separately from viewers. The same participant count can produce very different bills.
| Topology | Peer connections | Per-peer upload | Server media | Latency |
|---|---|---|---|---|
| P2P | 1 | 1 stream | None | Lowest |
| Mesh | n(n−1)/2 | (n−1) streams | None | Lowest for small rooms |
| SFU | n | 1 stream (plus layers) | Forwarded egress | Low |
| MCU | n | 1 stream | Mixed egress + high CPU | Higher |
| Cascaded SFUs | n per region + inter-SFU | 1 stream to local SFU | Regional + inter-region | Low if regions are close |
| Broadcast | n subscribers | Stage only | n × stream egress | Low 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.
Operations
Debug in order
Fix the earliest broken stage. A media symptom cannot tell you which layer failed until you inspect the sequence.
- 01
Secure context and permission
HTTPS or localhost, Permissions-Policy allows camera/mic, user granted access, and the correct device is selected.
- 02
Local tracks
Check getTracks() kinds, readyState, enabled, and muted. A black preview can be a disabled track or an unattached element.
- 03
Signaling
Socket open, join acknowledged, room present, offer and answer applied in the right order, target peer still present.
- 04
ICE
Candidates gathered, remote candidates added, candidate pair selected, TURN reachable. Read iceConnectionState and getStats pairs.
- 05
DTLS and media
Fingerprints match, DTLS connected, tracks received, element playing, inbound bytes increasing, autoplay policy satisfied.
- 06
Quality
RTT, loss, jitter, bitrate, frames dropped, qualityLimitationReason, CPU. Compare sender and receiver views.
- 07
Teardown
Tracks stopped, channels closed, peer connections closed, room membership removed, object URLs revoked.
| Symptom | First probe | Likely layer |
|---|---|---|
| No permission prompt | Secure context, Permissions-Policy, browser settings | Media capture |
| Camera preview works, remote is black | ontrack, autoplay, inbound bytes, track muted | Playback / autoplay |
| ICE stuck at checking | Candidate exchange, STUN/TURN, selected pair | Network / signaling |
| ICE connected, connection failed | DTLS fingerprint, a=setup, transport state | DTLS |
| Audio is robotic | packetsLost, jitter, bitrate, CPU | Network / codec |
| Data channel open, file stalls | bufferedAmount, bufferedamountlow, chunk size | Backpressure / SCTP |
| Works on localhost, fails on phone | HTTPS, LAN address, TURN, firewall | Secure context / network |
| Signaling dies, media freezes | Peer connection state, ICE state, selected pair | Network path / lifecycle |
Use browser WebRTC internals for a chronological event log. Use getStats for the current numeric truth. Use server logs for signaling state.
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.