Skip to content
On this pageOPEN
  1. 01Goal
  2. 02Plain-language concept
  3. 03Interactive diagram
  4. 04Behind the scenes
  5. 05Code walkthrough
  6. 06Do & don't
  7. 07Practice
  8. 08Quick quiz
  9. 09Key takeaways
  10. 10Common errors
LESSON 05Core28 min read

Signaling: the missing link

Design a WebSocket signaling protocol with join, offer, answer, ICE, and leave messages that survives real rooms.

#signaling#WebSocket#perfect negotiation#rooms
01

Goal

  • 01Design a signaling protocol with explicit message types, authorization, and room lifecycle.
  • 02Handle the ordering problems that cause most 'WebRTC is broken' reports.
  • 03Survive reconnects without duplicating peers, offers, or room membership.
02

The concept, plainly

is the out-of-band exchange that gets two peers to a mutually understood session. It carries offers and answers plus s. The WebRTC spec deliberately leaves the transport and protocol to you, because every product has different identity, presence, and room rules. That freedom is also why signaling is where most bugs live.

A useful protocol is explicit. Each message names its type, the sender, the target when relevant, and a payload version. The server should validate every message with a schema and refuse actions the peer is not authorized to take. 'The client is trusted' is not a security model; a browser is an attacker-controlled environment.

Room membership is server state. When a peer joins, the server assigns a connection id, returns the current peer list, and broadcasts the new arrival to everyone else. Who creates the offer? Pick a deterministic rule. In this site's 1:1 lab, the newcomer offers to an existing peer. In a mesh, each new peer offers to every existing peer. The rule must be written down, or both sides will glare.

Candidates and descriptions can arrive out of order. A remote candidate may be delivered before the remote description, especially on mobile networks. Queue candidates until `setRemoteDescription` succeeds. An answer can arrive while the local page is still gathering candidates; that is normal. Never assume the order of independent network messages.

Reconnection is part of the protocol, not an edge case. The signaling socket can drop while the media path stays healthy. When the socket returns, the peer needs a new presence record, and the room needs to know the old peer is gone. Use idempotent joins, a server-side heartbeat, and a new session id after reconnect. Do not blindly create a second peer connection.

Authentication should happen before room access. A short-lived token, a session cookie, or an authenticated WebSocket upgrade all work. Authorize room joins and message relays on the server. For scale, publish signaling between instances through Redis or another broker, but keep presence authoritative or clearly partitioned. Signaling is small in bytes but critical in consistency.

03

See it move

PLAY / PAUSE / STEP

Interactive diagram

A room-based signaling exchange

validatejoinjoinfan-outoffer / answer / ICEBROWSERPeer Aalready in roomNETWORKWS connection AauthenticatedSERVERAuthtoken → principalSTORAGERoom statemembers + presenceNETWORKWS connection BnewcomerBROWSERPeer Boffers to ASERVERBrokerRedis / queue (optional)NETWORKDirect mediaSRTP, not signaling
STEP 01 / 09auth

Open an authenticated control channel

The browser connects to the signaling endpoint. The server validates the session before accepting messages.

The signaling server coordinates rooms and relays control messages. Media never travels through this socket.

04

Behind the scenes

01

The server is a state machine too

A signaling server maintains peers, rooms, heartbeats, and rate limits. Each message mutates that state or relays to another participant. Validate the envelope before dereferencing a room, check that the sender is actually in the room, and verify the target exists. Otherwise a malformed 'offer to nobody' becomes a stuck call with no useful error.

02

Glare is your protocol's problem

Glare happens when both peers create offers at the same time. The WebRTC API allows it, but the result is an SDP collision that leaves the session in have-local-offer. Choose an initiator by comparing peer ids or room roles. If both sides can renegotiate later, implement the perfect negotiation pattern with polite and impolite roles.

03

Scaling is mostly routing

One process can hold many WebSocket connections for a learning lab. At production scale, a socket may live on one instance while its peer's socket lives on another. A broker routes messages by peer or room, and presence has a single owner or a durable store. The media path is unaffected; this is purely control-plane design.

05

Code walkthrough

COPY-READY

A typed protocol prevents unknown fields, oversized payloads, and accidental room escapes. Reject early with an error the client can display.

server/signaling.ts
import { z } from "zod";
const roomId = z.string().trim().regex(/^[a-zA-Z0-9_-]{2,40}$/);
const sdp = z.object({ type: z.enum(["offer", "answer"]), sdp: z.string().max(100_000) });
const candidate = z.object({
candidate: z.string().max(2_000),
sdpMid: z.string().nullable().optional(),
sdpMLineIndex: z.number().int().nullable().optional(),
usernameFragment: z.string().nullable().optional(),
});
export const clientMessage = z.discriminatedUnion("type", [
z.object({ type: z.literal("join"), room: roomId, name: z.string().trim().min(1).max(32) }),
z.object({ type: z.literal("offer"), to: z.string().min(1), data: sdp }),
z.object({ type: z.literal("answer"), to: z.string().min(1), data: sdp }),
z.object({ type: z.literal("ice"), to: z.string().min(1), data: candidate }),
z.object({ type: z.literal("leave") }),
]);
export function parseClientMessage(raw: string) {
const parsed = clientMessage.safeParse(JSON.parse(raw));
if (!parsed.success) return { ok: false as const, error: "INVALID_MESSAGE" };
return { ok: true as const, message: parsed.data };
}
06

Do and don't

Do

  • Define the initiator rule

    Write down who offers when a peer joins. The newcomer-offers rule used in this site avoids most 1:1 collisions.

  • Validate and authorize on the server

    Check schema, payload size, room membership, and target existence. A browser can send anything, including messages you never designed.

  • Queue candidates until the description exists

    This one habit prevents a large class of connections that stay stuck in checking.

  • Track connection generations

    After reset or rejoin, stale candidates and answers must not be applied to the new peer connection. Include a session or generation id.

Don't

  • Don't put media through signaling

    SDP is small control data. Audio, video, and files belong on SRTP or SCTP, not JSON over WebSocket.

  • Don't broadcast offers to every peer

    Target messages explicitly. A room-wide SDP broadcast invites glare and leaks session details to unrelated peers.

  • Don't trust client-supplied peer ids

    The server should issue identity and authorize targets. Otherwise a client can relay messages into rooms it does not belong to.

  • Don't reconnect silently forever

    Backoff with jitter, surface repeated failure, and stop on policy or auth errors. Infinite reconnect loops hide the real problem.

07

Practice

Hands-on lab

Read a real signaling trace

The 1:1 lab joins a real WebSocket room and logs every offer, answer, and candidate. Kill the socket while media is connected and observe what survives.

  1. 1Join a room from two tabs and inspect the order of joined, peer-joined, offer, answer, and ICE events.
  2. 2Identify which tab initiated the offer and why the room rule chose it.
  3. 3Kill the signaling socket while connected. The peer connection should stay connected.
  4. 4Reset and rejoin, then watch the server issue a new peer id and the room reconcile presence.
Open the signaling + 1:1 lab

Solo? Run loopback and compare the peer-event timeline with a manual SDP exchange to see which parts are signaling and which are peer connection state.

08

Quick quiz

Knowledge check

0 of 4 answered

0%
  1. 01

    Why must a signaling server validate messages even when the client is your own app?

  2. 02

    A remote ICE candidate arrives before the remote description. What should happen?

  3. 03

    What makes a deterministic initiator rule useful?

  4. 04

    The signaling socket disconnects after media is connected. What is the expected behavior?

Pick one answer per question to see instant feedback.
09

Key takeaways

  1. Signaling is a protocol you design, not a WebRTC API you call.01
  2. Rooms, identity, and initiator rules live on the server; ordering and generation ids live on the client.02
  3. Queue remote candidates and never assume message ordering.03
  4. Media can outlive signaling. Treat them as separate systems with separate health checks.04
10

Common errors and fixes

Terms in this lesson

Done with this lesson?

Mark it complete, then keep the thread.