Peer-to-Peer Multiplayer with WebRTC
WebRTC (Web Real-Time Communication) was originally designed for voice and video calling between browsers, but its data channel feature is equally valuable for games. Unlike WebSockets, which run over TCP and guarantee ordered delivery, WebRTC data channels can be configured to drop late packets rather than holding up all subsequent data waiting for a retransmission. For games sending position updates 20 to 60 times per second, a stale position update from 200 milliseconds ago is worthless. Dropping it and using the latest one produces a smoother experience than waiting for TCP to retransmit it.
Set Up a Signaling Server
WebRTC connections cannot be established without a signaling server. Before two browsers can communicate directly, they need to exchange connection metadata (SDP offers and answers) and network candidates (ICE candidates). This exchange happens through a signaling server that both peers are connected to, typically a simple WebSocket relay.
The signaling server's role is temporary and lightweight. It relays messages between peers during connection setup, which takes a few seconds. Once the peer-to-peer connection is established, the signaling server is no longer involved in data transfer. It can remain connected for coordination purposes (signaling new peers joining, handling disconnection notifications) but carries none of the game traffic.
A signaling server is essentially a WebSocket server with room-based message routing. When player A wants to connect to player B, player A sends an SDP offer through the signaling server addressed to player B. The server forwards it. Player B creates an SDP answer and sends it back through the signaling server. Both peers also send ICE candidates through the server as they are discovered. The server needs no understanding of the content, it simply routes messages between specific peers.
For production, the signaling server should authenticate players, prevent unauthorized connections, and track which players are in which rooms. But the core functionality is just message forwarding, making it extremely lightweight. A single Node.js process can handle signaling for thousands of concurrent peer connections because it does minimal processing per message.
Configure STUN and TURN Servers
Most devices on the internet sit behind NAT (Network Address Translation) routers that assign private IP addresses to local devices and share a single public IP. For peer-to-peer connections to work, each browser needs to discover its public-facing IP address and port. This is where STUN (Session Traversal Utilities for NAT) servers come in.
A STUN server is a simple service that a browser contacts to learn its own public IP and port as seen from the outside internet. The browser sends a request to the STUN server, and the server responds with the public address it sees. This information becomes an ICE candidate that the browser shares with the other peer through the signaling server. Google provides free public STUN servers (stun:stun.l.google.com:19302) that work for development and production, and several other free options exist.
TURN (Traversal Using Relays around NAT) servers are needed when direct peer-to-peer connections fail. This happens when both peers are behind symmetric NATs (common on mobile networks and corporate firewalls) where STUN-discovered addresses are not routable. A TURN server acts as a relay, forwarding data between peers that cannot connect directly. This adds latency (the data travels through the relay instead of directly between peers) and costs money (you must host or rent the TURN server, and it consumes bandwidth for every active session).
In practice, roughly 80 to 85 percent of peer connections succeed with STUN only (direct connection). The remaining 15 to 20 percent require TURN relay. For a game, you must configure TURN servers or accept that some players cannot connect. Services like Twilio, Xirsys, and Metered provide hosted TURN servers with pay-per-use pricing. Self-hosting a TURN server using the open-source coturn project is also straightforward if you have a server with a public IP.
Establish the Peer Connection
The connection process follows a specific sequence. The initiating peer (the "offerer") creates an RTCPeerConnection object, configures it with STUN and TURN server URLs, creates an SDP offer using createOffer(), sets the offer as the local description with setLocalDescription(), and sends it to the other peer through the signaling server.
The receiving peer (the "answerer") receives the offer, creates its own RTCPeerConnection, sets the received offer as the remote description with setRemoteDescription(), creates an SDP answer with createAnswer(), sets the answer as the local description, and sends it back through the signaling server.
Meanwhile, both peers generate ICE candidates as the browser probes available network interfaces and contacts STUN servers. Each candidate is sent to the other peer through the signaling server and added with addIceCandidate(). The peers try to connect using each candidate pair until they find one that works. This process is called ICE negotiation and typically completes within 1 to 5 seconds.
Once a working candidate pair is found, the peer connection enters the "connected" state. The oniceconnectionstatechange event notifies you when this happens. At this point, data can flow directly between the two browsers without going through any server (unless a TURN relay was needed).
Create and Configure Data Channels
Data channels are the actual pipes through which game data flows. The offerer creates a data channel before the offer is made using peerConnection.createDataChannel("game", options). The answerer receives the data channel through the ondatachannel event on its peer connection.
The configuration options determine how the data channel behaves. For game data, the most important settings are ordered: false (messages can arrive out of order, avoiding head-of-line blocking) and maxRetransmits: 0 (no retransmission of lost packets, behaving like UDP). This gives you the lowest possible latency at the cost of reliability. For game state updates that are sent every tick, this is the correct tradeoff because the next update replaces the lost one anyway.
You can create multiple data channels on the same peer connection for different purposes. Use an unreliable channel for position updates and an ordered, reliable channel for chat messages and critical game events (like scoring or game-over signals). Each channel operates independently, so reliability settings on one channel do not affect others.
Data channels support both string and binary (ArrayBuffer) messages, just like WebSockets. For game data, use binary messages with the same kind of binary protocol you would use with WebSockets. The encoding and decoding code can be shared between your WebSocket and WebRTC implementations if your game supports both transport options.
Build the Peer Mesh or Star Topology
With two players, you have one peer connection. With more players, you need a network topology. The two common options are mesh and star (host-based).
In a mesh topology, every player connects to every other player. With 4 players, each player maintains 3 peer connections, for a total of 6 connections in the room. Each player sends their state to all other players and receives state from all of them. Mesh is simple and fully distributed (no single point of failure), but it scales poorly. With N players, each player needs N-1 connections, and total connection count is N*(N-1)/2. At 8 players, that is 28 connections. At 16 players, it is 120 connections. The bandwidth requirement for each player also scales linearly with the number of peers. Mesh works well for 2 to 6 players.
In a star topology, one player is designated as the host. All other players connect only to the host. The host runs the game simulation (acting as the authoritative server), receives inputs from all players, processes them, and sends state updates to everyone. This requires only N-1 connections total (each non-host player connects to the host). It scales better than mesh, but the host has a latency advantage (zero latency to the server, since they are the server) and if the host disconnects, the game must either end or perform a host migration (transferring game state to another player and establishing new connections).
Host migration is complex but important for star topologies. When the host disconnects, the remaining players need to elect a new host (typically the player with the lowest latency or the one who joined first), transfer the last known game state to the new host, and reconnect all peers to the new host. This process takes several seconds during which the game is frozen. Implement it if your game sessions are long enough that host disconnections are likely. For short matches (under 5 minutes), simply ending the match when the host disconnects may be acceptable.
WebRTC data channels provide UDP-like low-latency transport for browser games, but come with significant setup complexity. Use them when you need unreliable messaging for fast-paced games, want to avoid server hosting costs, or need peer-to-peer connections for small-session casual games. For most multiplayer web games, WebSockets are simpler and more reliable, and WebRTC should be considered an optimization for latency-critical use cases.