Why WebRTC Projects Fail After the Demo
WebRTC makes it possible to send audio, video, and arbitrary data directly between browsers. The first demo can be surprisingly small: call getUserMedia, create an RTCPeerConnection, exchange an offer and answer, and render a remote stream.
The difficult part begins when real users arrive. They join from a corporate network, switch from Wi-Fi to mobile data, close their laptop, open a second tab, or lose the signaling connection while the media path is still alive. A reliable product treats those events as normal operating conditions.
The Three Systems You Are Building
WebRTC is a transport building block, not a complete application architecture. Separate the problem into three systems:
identity and rooms
|
signaling: offer, answer, ICE candidates
|
connectivity: ICE, STUN, TURN, DTLS, SRTP
|
media and data: tracks, codecs, data channels
Signaling tells peers how to find each other. Connectivity negotiates a route through NATs and firewalls. Media and data carry the application payload after a route is selected. Keeping these boundaries clear makes failures easier to diagnose.
What Happens During a Connection
The browser creates an offer containing capabilities such as supported codecs, audio channels, and data-channel options. The offer is sent through your signaling service to the other peer. That peer creates an answer, and both sides gather ICE candidates.
Candidates describe possible paths:
- A host candidate is a local interface address.
- A server-reflexive candidate is a public address discovered through STUN.
- A relay candidate is an address allocated by a TURN server.
ICE tests candidate pairs and selects a working route. After that, DTLS establishes encryption keys and SRTP carries media securely. The application does not need to implement encryption itself, but it does need to observe whether the negotiation reached each stage.
Signaling: Small Protocol, Serious State
WebRTC does not specify signaling. WebSockets are a common choice because the server can push messages immediately, but the transport matters less than the protocol you design around it.
type SignalMessage =
| {
type: "offer" | "answer";
sessionId: string;
description: RTCSessionDescriptionInit;
}
| {
type: "ice-candidate";
sessionId: string;
candidate: RTCIceCandidateInit;
};
socket.on("signal", (message: SignalMessage) => {
if (message.sessionId !== currentSessionId) return;
peerConnection.signal(message);
});
Every message should include a room ID, sender ID, session ID, and protocol version. The session ID matters because a user can reconnect while an old connection is still shutting down. Without it, delayed messages from the old session can corrupt a new negotiation.
Make joining idempotent. If a browser retries a join-room request, the server should return the existing membership instead of creating a duplicate participant. Store room membership on the server; never use the browser’s local participant list as the source of truth.
Glare is another common production bug. Glare happens when both peers create an offer at the same time. Pick a deterministic owner for negotiation, such as the participant with the lower ID, or implement the “perfect negotiation” pattern with polite and impolite peers. Otherwise the call can work in testing and randomly fail when two users enable screen sharing at the same time.
STUN Helps, TURN Makes It Work
Most clients are behind NAT. STUN lets a browser discover the public address mapped by its router. That is enough for many home networks, but not for every network topology.
TURN relays traffic when a direct route cannot be established. It increases bandwidth cost and latency, but it is essential for restrictive corporate networks, symmetric NATs, and some mobile carriers. A useful deployment usually has:
browser -> signaling service (WebSocket)
browser <-> browser (direct media when possible)
browser -> TURN -> browser (fallback media path)
Use short-lived TURN credentials generated by your backend. Do not ship a permanent TURN password in frontend code. Track the selected candidate type so you know how often your service relies on relays and can budget for the bandwidth.
Tracks, Transceivers, and Device Changes
Use tracks for the media itself and transceivers when you need to control direction or replace a source without renegotiating everything. For example, camera switching should normally replace the sender’s track:
const sender = peerConnection
.getSenders()
.find(item => item.track?.kind === "video");
if (sender) await sender.replaceTrack(newCameraTrack);
Stop tracks from devices you no longer use. A camera that remains active after a user turns video off wastes battery and can prevent another application from accessing the device. Listen for devicechange and provide a clear recovery path when a USB microphone disappears.
Media Quality Is a Control Problem
A fixed 1080p stream is not a quality strategy. Capture and send quality should react to available bandwidth, CPU, screen size, and the number of participants. Useful metrics include round-trip time, packets lost, jitter, frames per second, and the current resolution.
For a sender, RTCRtpSender.getParameters() exposes encodings that can be adapted. In a group call, simulcast can publish multiple quality layers so a small mobile tile does not receive the same bitrate as a full-screen speaker.
Do not hide a poor connection behind a spinner. Tell the user whether the problem is reconnecting, a missing permission, a muted microphone, or a degraded network. Good status text is part of reliability because it prevents users from taking destructive actions such as repeatedly joining the same room.
For group calls, separate capture quality from subscription quality. A presenter may upload a high-quality stream, while most viewers receive a lower layer. Server-side forwarding units, or SFUs, make this practical by forwarding selected simulcast or scalable-video-coding layers instead of mixing everything into one stream. Mesh calls are simple for two or three users, but bandwidth grows quickly because every participant sends media to every other participant.
mesh: user sends N-1 streams
SFU: user sends 1 stream, receives selected streams
MCU: server mixes streams, users receive one composed stream
Data Channels Need Message Semantics
Data channels are useful for cursor positions, game events, whiteboard strokes, and collaborative state. Choose the channel’s reliability based on the message:
| Message | Good policy |
|---|---|
| Cursor position | Unordered and unreliable; latest state wins |
| Chat message | Ordered and reliable |
| Whiteboard operation | Reliable with sequence numbers |
| Presence heartbeat | Unreliable with a timeout |
The bufferedAmount property is backpressure. If it keeps increasing, the producer is generating messages faster than the network can send them. For high-frequency state, keep only the latest value. For commands, assign sequence numbers and acknowledge them so the receiver can detect gaps.
Recovery Is a State Machine
Do not treat connected as a permanent state. Observe connectionState, iceConnectionState, and the signaling connection separately.
new -> connecting -> connected
\-> disconnected -> checking -> connected
\-> failed -> rebuilding
When ICE becomes disconnected, wait briefly because mobile networks often recover. Then attempt an ICE restart. If negotiation is corrupt or the peer connection is failed, create a fresh RTCPeerConnection and re-add tracks. Keep room membership and application state outside the connection object so rebuilding the transport does not remove the user from the room.
Security and Privacy Boundaries
Authenticate the signaling connection and authorize every room action on the server. A room ID is not permission. Validate that a user can join the room, publish a track, subscribe to a participant, and send a data message.
Consider what metadata you expose. Even when media is encrypted, a server may learn room membership, timing, IP addresses, and relay usage. Restrict logs, expire session records, and avoid putting private content into signaling messages unnecessarily.
WebRTC media is encrypted with DTLS-SRTP, but that does not automatically make the full product private. If you record calls, run transcription, or route through an SFU, those server components become part of the trust boundary. Document who can access recordings, how long diagnostics are retained, and whether TURN logs contain IP addresses.
Failure Playbook
Production teams need an answer for each common failure, not just a retry button:
| Symptom | Likely area | Useful response |
|---|---|---|
| No remote media | Signaling or ICE | Show connection state and inspect candidate exchange |
| Works at home, not work | TURN or firewall | Verify relay allocation and UDP/TCP/TLS fallback |
| Audio breaks on mobile | Network handoff | Attempt ICE restart and preserve room state |
| Video freezes | Uplink loss or CPU | Lower bitrate, frame rate, or selected layer |
| Duplicate participant | Session lifecycle | Make joins idempotent and expire stale sessions |
Observability That Actually Helps
Record a correlation ID for the room session and collect:
- Time to signaling connection and time to first media.
- ICE candidate type and selected protocol.
- Connection state transitions and recovery attempts.
- Round-trip time, packet loss, jitter, bitrate, resolution, and frame rate.
- Browser, operating system, network type, and codec.
Never log raw audio, video, or credentials. A dashboard showing “calls failed” is less useful than one showing “TURN allocation failures increased on one region” or “mobile users have high uplink loss after camera switching.”
Production Checklist
- Test two users behind restrictive NATs, not just two localhost tabs.
- Test refreshes, duplicate joins, delayed signaling, and stale sessions.
- Test Wi-Fi to mobile handoff, laptop sleep, background tabs, and device removal.
- Configure TURN with expiring credentials and monitor relay traffic.
- Use adaptive quality and make degraded states visible to users.
- Add backpressure and sequence numbers to data channels.
- Authorize room actions on the server and protect signaling endpoints.
- Collect connection metrics without recording private media.
WebRTC handles a remarkable amount of transport complexity. The application still owns lifecycle, identity, recovery, permissions, and user experience. Designing those parts deliberately is what turns a convincing demo into a dependable real-time product.