Perfect Negotiation is a recommended pattern for handling WebRTC renegotiation in a symmetric way, where either peer can start a renegotiation at any time without the two sides stepping on each other.
It is documented in the W3C WebRTC specification as the canonical way to use the negotiationneeded event together with setLocalDescription() and setRemoteDescription().
The problem Perfect Negotiation solves
Classic WebRTC signaling assumes one peer is the caller and the other is the callee. The caller creates the offer, the callee answers, and renegotiation follows the same direction.
That model breaks down as soon as both peers can change the session – adding a track, switching cameras, or triggering an ICE restart. If both sides happen to generate an offer at the exact same moment, the offers collide. This is known as glare in telecom terminology. Without a tie-breaker, you end up with:
- Both peers in
have-local-offerstate, each waiting for the other to answer - Out-of-order SDP exchanges that leave the PeerConnection in an inconsistent state
- Races where one peer’s changes overwrite the other’s
Perfect Negotiation gives each side a deterministic rule so the collision always resolves cleanly.
Polite and impolite peers
The core idea is to assign one peer as polite and the other as impolite. This is decided once at session setup (typically based on who initiated the call, or a random coin flip exchanged via signaling).
- Impolite peer: Stands its ground. If an offer arrives while it already has a pending local offer, it ignores the incoming offer
- Polite peer: Yields. If an offer arrives while it has a pending local offer, it rolls back its own offer and accepts the remote one
The roles are arbitrary but must be agreed in advance. As long as exactly one side is polite, glare is always resolved the same way.
How the pattern works
Perfect Negotiation ties together three WebRTC mechanisms:
negotiationneededevent fires whenever the PeerConnection needs a new offer (a track was added, ICE restart was requested, etc.)- Rollback is a special SDP type (
{type: 'rollback'}) that cancels a pending local offer and returns the PeerConnection tostable - Offer/answer collision detection happens inside the handler for incoming offers – the polite peer checks whether it has a pending local offer and rolls back if so
The handler does not need to know *why* renegotiation is happening – adding a track, removing a track, or restarting ICE all flow through the same path.
Perfect Negotiation and ICE restart
ICE restart is one of the cleanest examples of where Perfect Negotiation pays off. Calling pc.restartIce() fires negotiationneeded, which then runs through the same offer/answer flow as any other change. The polite/impolite rules handle the case where the other side is also renegotiating at the same moment – for example, adding a track while you are trying to recover connectivity.
That is why restartIce() is preferred over the older createOffer({ iceRestart: true }) call: it integrates naturally with the Perfect Negotiation handler instead of requiring a separate code path.
When Perfect Negotiation is worth adopting
Perfect Negotiation adds some signaling complexity, so it is overkill for strictly one-sided flows (classic caller/callee with no dynamic track changes).
It becomes valuable when:
- Either peer can add or remove tracks mid-call (screen sharing toggles, camera switches)
- You want automatic ICE restart on connectivity failure
- Your signaling channel is unreliable or out-of-order
- You are building a peer-to-peer application without a central server dictating who leads
For applications that fit this profile, Perfect Negotiation fits well in resolving signaling edge cases.


