Skip to content
Harshal Patel
Go back

Building Reliable Real-Time Apps with WebRTC

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.

A network operations room representing real-time communication infrastructure

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:

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.

A control room dashboard representing signaling and session state

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:

MessageGood policy
Cursor positionUnordered and unreliable; latest state wins
Chat messageOrdered and reliable
Whiteboard operationReliable with sequence numbers
Presence heartbeatUnreliable 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:

SymptomLikely areaUseful response
No remote mediaSignaling or ICEShow connection state and inspect candidate exchange
Works at home, not workTURN or firewallVerify relay allocation and UDP/TCP/TLS fallback
Audio breaks on mobileNetwork handoffAttempt ICE restart and preserve room state
Video freezesUplink loss or CPULower bitrate, frame rate, or selected layer
Duplicate participantSession lifecycleMake joins idempotent and expire stale sessions

Observability That Actually Helps

Record a correlation ID for the room session and collect:

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

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.


Share this post:

Previous Post
Web Dev Today Is Like LEGO — And AI Is Good at It
Next Post
Building an Ultra-Low Latency Order Book — Low-Level Design for High-Frequency Trading