WebRTC Cheatsheet

Connection States

Use this WebRTC reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

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 });
}