WebRTC guide

How a WebRTC Peer-to-Peer Transfer Actually Connects

The real mechanics behind a browser-to-browser data channel: signalling, the SDP offer and answer, ICE candidate gathering, STUN and TURN, NAT behaviour, DTLS encryption, and when a relay becomes unavoidable.

A peer-to-peer transfer looks like magic from the outside — two browsers, a short code, and a file moves between them without an upload. Underneath it is a negotiation with several distinct phases, each of which can fail for its own reason. Knowing the phases is the difference between "it doesn't work" and "both sides gathered only relay candidates and no TURN server is configured", which is a problem you can act on.

Signalling is not part of WebRTC

The specification deliberately leaves out how the two peers first reach each other. WebRTC defines what has to be exchanged — a session description and a set of network candidates — but not the channel used to exchange it. You can use a WebSocket, an HTTP endpoint, a QR code, email, or a person reading a string aloud over the phone. All of them work, because the payload is just text.

That design decision is why every peer-to-peer tool needs some out-of-band step. On this site, the room-code mode passes the exchange through a same-origin endpoint that behaves as a small mailbox. The room lives in the SSR process's own memory, holds at most two members, accepts payloads up to 96 KB, and expires after 10 minutes idle — it is held open while a connection is active and released on disconnect or timeout. It stores negotiation text and nothing else: no file ever passes through it.

The manual-code mode removes even that. The session description is compressed into an invite code that you carry to the other side yourself, and the answer comes back the same way. Nothing is requested from the server at all. It is more work and it is the right choice when you would rather not have the negotiation text touch any server, including this one.

The offer and the answer

The initiating peer creates a session description — the offer — and the other side replies with an answer. Both are SDP documents, a line-oriented text format inherited from older signalling protocols. For a data-channel-only transfer the document is short, and four lines in it carry almost all the meaning.

The m= line declares the media section; for a file transfer it is an application section carrying an SCTP association rather than audio or video. The a=ice-ufrag and a=ice-pwd lines are short-lived credentials that the two sides use to authenticate ICE connectivity checks to each other, which stops an unrelated host from injecting checks into the exchange. The a=fingerprint line is the SHA-256 hash of the peer's DTLS certificate, and the a=setup line says which side will act as the DTLS client.

The fingerprint deserves attention because it is where the security of the whole transfer rests. The data channel is encrypted with DTLS using a certificate the browser generated, and the only thing binding that certificate to the person you think you are talking to is the fingerprint you received through signalling. If an attacker can rewrite the signalling payload, they can substitute their own fingerprint and sit in the middle. That is the real reason the invite code matters: it is not a password protecting a room, it is the integrity of the key exchange.

A data-channel offer, trimmed to the lines that matter
v=0
o=- 4611731400430051336 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE 0
m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=ice-ufrag:F7gI
a=ice-pwd:x9cml6YzichV2QXlhiMu8g
a=fingerprint:sha-256 4A:AD:B9:B1:3F:82:18:3B:54:02:12:DF:3E:5D:49:6B:
                      19:E5:7C:AB:3A:0B:3D:2C:1E:0F:7A:66:55:44:33:22
a=setup:actpass
a=mid:0
a=sctp-port:5000
a=max-message-size:262144

# c=IN IP4 0.0.0.0 is normal: the real addresses arrive as
# candidate lines, either inside the offer once gathering has
# finished, or one at a time if you are trickling them.

ICE candidates and what each type tells you

While the offer is being prepared, the browser gathers candidates: every address and port through which it might be reachable. Each candidate carries a type, and the type is the single most diagnostic field in the whole process. Reading the candidate list of a failed connection usually tells you immediately what went wrong.

Candidates are paired across the two peers and tested in priority order using STUN connectivity checks, a process defined by ICE (RFC 8445). The priority is computed as (2^24 x type preference) + (256 x local preference) + (256 - component), which with the recommended type preferences produces 2130706431 for a host candidate, 1694498815 for a server-reflexive one, and 16777215 for a relay. That ordering is why a LAN transfer between two laptops on the same Wi-Fi connects almost instantly and never touches a relay: the host pair wins the race outright.

  • Only host candidates and no srflx candidate means STUN did not answer — check that a STUN server is configured and that UDP is not being blocked.
  • Only relay candidates on one side is normal on restrictive networks; only relay candidates and no TURN server means the connection cannot be established at all.
TypeWhere the address comes fromType preferenceWhat it implies
hostA local interface on the device itself126Direct connection on the same network. Browsers publish these as a random .local mDNS name, so page script never sees the private address
prflx (peer reflexive)Learned during connectivity checks, from a mapping neither side predicted110The NAT gave the peer a different mapping than it gave the STUN server — a sign of address-dependent mapping
srflx (server reflexive)Reported by a STUN server: the public address and port the NAT assigned100The normal route for a direct connection across the internet
relayAllocated on a TURN server, which forwards every byte0Works whenever the TURN server is reachable, at the cost of bandwidth, latency and someone paying for it

NAT behaviour decides whether a direct path exists

STUN works by telling a peer what its address looks like from outside. Whether that address is usable by a third party depends entirely on how the NAT allocates mappings, and RFC 4787 gives the vocabulary. A NAT with endpoint-independent mapping reuses the same external port for a given internal socket no matter where the traffic is going, which means the address STUN reported is the address the peer can use. This is the common case on home routers and it is why hole punching usually works.

A NAT with address-and-port-dependent mapping — often called symmetric — allocates a fresh external port for every distinct destination. The mapping the STUN server observed is therefore useless to the peer, because the peer is a different destination and will be given a different port. When one side behaves this way the connection can often still be rescued, because the other side's predictable mapping gives the checks something to aim at, and the resulting discovery shows up as a peer-reflexive candidate. When both sides behave this way there is no address to punch toward, and a relay is not a fallback, it is the only option.

STUN, TURN, and why this site does not run a relay

The two servers have very different cost profiles, which is why they are treated differently everywhere. A STUN server answers one small request with the address it saw and then forgets you; it carries no user traffic and costs almost nothing to run. A TURN server allocates a port on behalf of a peer and forwards every single byte of the session in both directions, which means it pays for the full bandwidth of every transfer that uses it. TURN also authenticates, because an open relay is an abuse magnet.

This site does not operate or host a TURN relay, and the peer-to-peer tool ships no default one. It includes a directory of public STUN addresses you can select from, add to, remove, and test individually, and it lets you enter your own STUN or TURN servers with credentials. With no ICE servers configured at all, the tool still attempts a local-network direct connection, which is enough for two machines on the same Wi-Fi. For anything crossing networks you supply the servers.

Inside the data channel

Once a candidate pair is selected, the two sides perform a DTLS handshake over it and verify the certificate against the fingerprint from the SDP. Everything above that runs as SCTP encapsulated in DTLS, which is what a data channel is. Encryption is not optional and cannot be turned off: there is no unencrypted mode in WebRTC.

A data channel is reliable and ordered by default, which is what you want for a file: bytes arrive exactly once and in sequence, with retransmission handled for you. You can trade that away with maxRetransmits or maxPacketLifeTime for latency-sensitive data, but for a transfer the default is correct. What you cannot ignore is message size. Implementations negotiate a maximum message size — 262144 bytes in the example above — and sending a whole file as one message will fail. Chunk the file, and read the negotiated limit rather than assuming it.

The other practical issue is backpressure. Calling send in a tight loop fills an in-memory queue far faster than the network drains it, and a large file will exhaust memory before it finishes. Watch bufferedAmount, set bufferedAmountLowThreshold, and pause the reader until the buffered amount falls below it. A transfer that starts fast and then stalls or crashes the tab is almost always this and not a network problem.

  • Chunk at or below the negotiated max-message-size; 64 KB is a widely safe chunk and 16 KB is the most conservative choice.
  • A transfer that always dies at the same byte offset is a chunking or buffering bug, not a NAT problem.

What to remember

  • WebRTC does not define signalling, so every peer-to-peer tool needs an out-of-band step — here a room mailbox in SSR process memory that holds only negotiation text, expires after 10 minutes idle, and never sees a file.
  • The SDP fingerprint binds the DTLS certificate to the peer, so protecting the integrity of the invite code is what prevents a machine-in-the-middle.
  • Read the candidate types first when a connection fails: no server-reflexive candidate means STUN did not answer, and relay-only candidates with no TURN server means the connection cannot complete.
  • A direct path depends on NAT mapping behaviour; when both sides use address-and-port-dependent mapping, a relay is the only option, and this site does not run one.
  • Chunk below the negotiated max-message-size, respect bufferedAmount backpressure, and hash both copies to confirm the file arrived intact.

Continue with related checks and tools