WebRTC Cheatsheet

Connection States

Use this WebRTC reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Three Independent State Machines

RTCPeerConnection exposes three separate state properties — they evolve independently and all must be understood:

PropertyTracksFinal states
signalingStateSDP negotiation progress"stable", "closed"
iceConnectionStateICE connectivity checks"connected", "completed", "failed", "closed"
connectionStateOverall DTLS + ICE rollup"connected", "failed", "closed"

Use connectionState for UI (it's the definitive rollup). Use iceConnectionState for diagnostics.

signalingState

Tracks where you are in the offer/answer exchange.

ValueMeaning
"stable"No negotiation in progress; safe to send data
"have-local-offer"Local offer set; waiting for remote answer
"have-remote-offer"Remote offer received; need to call createAnswer
"have-local-pranswer"Local provisional answer set
"have-remote-pranswer"Remote provisional answer received
"closed"Connection closed
pc.onsignalingstatechange = () => {
  console.log('Signaling state:', pc.signalingState);
};

// Guard renegotiation
pc.onnegotiationneeded = async () => {
  if (pc.signalingState !== 'stable') return; // skip if mid-exchange
  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);
  signaling.send({ type: 'offer', sdp: pc.localDescription });
};

iceConnectionState

Tracks ICE candidate pair selection.

ValueMeaning
"new"ICE not yet started
"checking"Sending STUN connectivity checks
"connected"A usable pair was found; data can flow
"completed"All checks done; optimal pair selected
"failed"No working pair found after all checks
"disconnected"Temporary loss of connectivity (may self-heal)
"closed"Connection closed
pc.oniceconnectionstatechange = () => {
  const state = pc.iceConnectionState;
  console.log('ICE:', state);

  if (state === 'failed') {
    // Attempt ICE restart
    restartIce();
  }

  if (state === 'disconnected') {
    // Don't close yet — give it ~5s to reconnect
    startDisconnectTimer();
  }

  if (state === 'connected' || state === 'completed') {
    clearDisconnectTimer();
  }
};

connectionState

The high-level rollup: reflects the combined state of ICE + DTLS.

ValueMeaning
"new"Not started
"connecting"Establishing ICE and DTLS
"connected"Fully established; media/data flowing
"disconnected"Connection interrupted; may self-heal
"failed"Could not connect or connection broke permanently
"closed"pc.close() was called
pc.onconnectionstatechange = () => {
  const state = pc.connectionState;

  switch (state) {
    case 'connected':
      showUI('Connected');
      break;
    case 'disconnected':
      showUI('Reconnecting…');
      scheduleIceRestart();
      break;
    case 'failed':
      showUI('Connection failed');
      pc.close();
      createNewConnection(); // full reconnect
      break;
    case 'closed':
      cleanup();
      break;
  }
};

iceGatheringState

Tracks ICE candidate collection progress.

ValueMeaning
"new"Gathering not started
"gathering"Actively collecting candidates
"complete"All candidates have been gathered
pc.onicegatheringstatechange = () => {
  console.log('Gathering:', pc.iceGatheringState);
};

// Wait for all candidates before sending offer (non-trickle ICE)
function waitForGathering(pc) {
  return new Promise(resolve => {
    if (pc.iceGatheringState === 'complete') return resolve();
    pc.onicegatheringstatechange = () => {
      if (pc.iceGatheringState === 'complete') resolve();
    };
  });
}

const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
await waitForGathering(pc);
// Now pc.localDescription contains all candidates
signaling.send({ type: 'offer', sdp: pc.localDescription });

State Transition Diagram

connectionState:

  new → connecting → connected → disconnected ⟶ failed
                   ↘                           ↗
                    closed ←─────────────────┘
                    (pc.close())
iceConnectionState:

  new → checking → connected → completed
              ↓                    ↓
           failed             disconnected → failed
              ↓                    ↓
            closed               closed

DataChannel readyState

dc.readyState; // 'connecting' | 'open' | 'closing' | 'closed'

dc.onopen    = () => { /* safe to send */ };
dc.onclose   = () => { /* channel gone */ };
dc.onerror   = (e) => { console.error(e.error); };
ValueMeaning
"connecting"Channel created, waiting for negotiation
"open"Ready — send() works
"closing"dc.close() called, draining
"closed"Channel terminated

MediaStreamTrack readyState

track.readyState; // 'live' | 'ended'

track.onended = () => { /* device disconnected or stop() called */ };
track.onmute  = () => { /* sending silence/black */ };
track.onunmute = () => { /* sending real data again */ };

Comprehensive State Monitor

function monitorConnection(pc, label = 'pc') {
  const log = (prop, val) => console.log(`[${label}] ${prop}: ${val}`);

  pc.onsignalingstatechange      = () => log('signalingState',     pc.signalingState);
  pc.oniceconnectionstatechange  = () => log('iceConnectionState', pc.iceConnectionState);
  pc.onicegatheringstatechange   = () => log('iceGatheringState',  pc.iceGatheringState);
  pc.onconnectionstatechange     = () => log('connectionState',    pc.connectionState);
  pc.onicecandidateerror         = (e) => log('iceCandidateError', `${e.errorCode}: ${e.errorText}`);

  pc.ontrack = ({ track, streams }) => {
    log('track', `${track.kind} ${track.id}`);
    track.onended  = () => log('trackEnded',  track.kind);
    track.onmute   = () => log('trackMuted',  track.kind);
    track.onunmute = () => log('trackUnmuted', track.kind);
  };
}

monitorConnection(pc, 'caller');

Reconnection Strategy

let iceRestartTimeout;

pc.onconnectionstatechange = async () => {
  if (pc.connectionState === 'disconnected') {
    // Give the browser 5 s to self-heal
    iceRestartTimeout = setTimeout(async () => {
      if (pc.connectionState !== 'connected') {
        await triggerIceRestart();
      }
    }, 5000);
  }

  if (pc.connectionState === 'connected') {
    clearTimeout(iceRestartTimeout);
  }

  if (pc.connectionState === 'failed') {
    clearTimeout(iceRestartTimeout);
    // ICE restart no longer possible from 'failed'; need a new PeerConnection
    pc.close();
    reconnect();
  }
};

async function triggerIceRestart() {
  const offer = await pc.createOffer({ iceRestart: true });
  await pc.setLocalDescription(offer);
  signaling.send({ type: 'offer', sdp: pc.localDescription });
}