Building a Multiplayer Three.js Game

Updated June 2026
Adding multiplayer to a Three.js game requires networking architecture that handles the realities of internet latency, packet loss, and the need for consistent game state across all connected players. The standard approach uses an authoritative server that runs the simulation, WebSocket connections for communication, client-side prediction to hide latency for the local player, and entity interpolation to smooth remote player movement. This guide covers each layer of that architecture.

Multiplayer networking is independent of the rendering library. The same techniques used in Unity or Unreal multiplayer games apply to Three.js games, because the challenges are about distributed state management and latency compensation, not about how pixels are drawn. What makes Three.js multiplayer unique is the JavaScript ecosystem: Node.js servers share language with the client, WebSocket is native to the browser, and the entire stack runs on technologies web developers already know.

Step 1: Choose a Network Architecture

The authoritative server model is the industry standard for competitive multiplayer games. The server runs the simulation, processes player inputs, resolves game logic, and broadcasts the authoritative state to all clients. Clients send only their inputs (key presses, mouse movement) and never modify game state directly. This prevents most forms of cheating because the server validates every action.

Peer-to-peer (P2P) architecture connects players directly without a central server. This reduces infrastructure costs and latency between players who are geographically close, but it makes cheat prevention nearly impossible and struggles with more than 4-8 concurrent players. P2P suits casual cooperative games where trust between players is assumed. WebRTC data channels provide the underlying transport for browser-based P2P connections.

A relay server is a middle ground: a lightweight server that forwards messages between clients without running the simulation. This provides NAT traversal (solving the connectivity problems that plague direct P2P) while keeping server costs low. However, it offers no cheat protection and still requires one client to be the "host" running the simulation, with the usual host advantage in latency.

For most Three.js games with competitive elements, start with an authoritative server. The additional development effort pays for itself in player trust and consistent gameplay.

Step 2: Set Up WebSocket Communication

WebSockets provide a persistent, bidirectional connection between the client and server with low overhead per message. Unlike HTTP, which requires a new request-response cycle for each interaction, a WebSocket connection stays open, and either side can send messages at any time. This is ideal for real-time games that exchange many small messages per second.

On the server side, use a Node.js WebSocket library. The native ws package is the fastest and most widely used. Socket.io adds automatic reconnection, room management, and fallback transport layers, but its additional overhead (larger messages, more processing per event) makes it less suitable for latency-sensitive games. For most Three.js games, raw WebSockets with the ws package provide the best balance of simplicity and performance.

Define a message protocol using a consistent format. Each message needs a type identifier and a payload. JSON is easy to debug but wasteful for frequent updates. Binary formats using ArrayBuffer and DataView are more efficient, reducing message size by 60-80% compared to JSON. For development, start with JSON and switch to binary once the protocol is stable. Libraries like MessagePack and FlatBuffers provide binary serialization with less manual work than raw DataView manipulation.

Step 3: Build the Authoritative Server

The server runs a headless version of your game simulation, meaning all game logic without any rendering. It maintains the authoritative game state: player positions, entity states, scores, inventories, and every other piece of data that defines the game world. The server processes player inputs at a fixed tick rate (typically 20-60 ticks per second), updates the simulation, and broadcasts state snapshots to all clients.

Structure the server around a game loop that runs at the tick rate using setInterval or a high-resolution timer. Each tick, process all queued player inputs in the order they were received, step the physics simulation (if using server-side physics), update AI, resolve game events, and assemble a state snapshot. Send the snapshot to all connected clients.

Share game logic code between server and client by extracting it into modules that both can import. Movement calculations, collision rules, weapon damage formulas, and ability mechanics should be identical on both sides. This is the main advantage of using Node.js for the server: the same JavaScript functions run on both client and server, ensuring consistency and reducing code duplication.

The server tick rate determines the update frequency sent to clients. A 20Hz tick rate sends 20 state updates per second, which is sufficient for most games and keeps bandwidth reasonable. Competitive shooters may need 60Hz or higher for responsive hit detection. Lower tick rates reduce server CPU and bandwidth costs but increase the perceived lag for players.

Step 4: Implement Client-Side Prediction

Without prediction, the local player presses a movement key and waits for the round-trip to the server before seeing any movement. With 100ms of latency, this 100ms delay makes the game feel sluggish and unplayable. Client-side prediction solves this by applying the player's input locally and immediately, using the same movement code the server uses, without waiting for server confirmation.

The client maintains a buffer of unacknowledged inputs, each tagged with a sequence number. When the player presses a key, the client applies the input locally, sends it to the server with its sequence number, and stores it in the buffer. When the server responds with the authoritative state and the last processed input sequence number, the client discards all inputs up to that number (they have been confirmed) and replays any remaining inputs on top of the server's state.

This replay step is called reconciliation. If the server agrees with the client's prediction (which it should, because they run the same code), the replayed state matches the client's current state and nothing visually changes. If there is a discrepancy (due to another player's action, server-side validation, or timing differences), the client snaps or interpolates to the corrected state. Small corrections are interpolated over a few frames to avoid visible teleporting.

Step 5: Add Entity Interpolation

Remote players (other players in the game) cannot use prediction because the local client does not have their inputs. Instead, the client renders them slightly in the past, using interpolation between two recent server state updates. This produces smooth movement at the cost of a small additional delay (one server tick interval, typically 50ms at 20Hz).

Maintain a buffer of the last few state updates for each remote entity. Each update includes a timestamp. During rendering, find the two updates that bracket the current render time (which is set to the current time minus the interpolation delay). Calculate the interpolation factor as the position between these two timestamps, and lerp the entity's position and rotation accordingly.

If state updates stop arriving (due to network jitter or packet loss), the client can extrapolate by continuing the entity's last known velocity. Extrapolation should be limited to a short duration (100-200ms) because extended extrapolation produces increasingly inaccurate results. If updates resume after a gap, blend from the extrapolated position to the new interpolated position to avoid a visible snap.

Step 6: Optimize Network Traffic

Bandwidth is a real constraint for browser games. Each client's WebSocket connection must handle the state updates flowing from the server, and the server must send updates to every connected client. A naive implementation that sends full game state every tick will overwhelm connections with more than a few players.

Delta compression sends only the changes since the last acknowledged update rather than the full state. If a player's position changed but their health and inventory did not, only the position is included in the message. Track which fields changed and assemble minimal update packets. This typically reduces message size by 70-90% for large game states.

Interest management (also called area of interest or relevance filtering) limits what each client receives based on proximity. A player on the east side of the map does not need updates about entities on the west side. The server tracks each client's relevant area and only includes nearby entities in their updates. This scales the bandwidth cost linearly with the number of nearby entities rather than the total entity count.

Use binary message formats for state updates. Position data as three 32-bit floats (12 bytes) is far more compact than the JSON string representation (potentially 40+ bytes). Define a binary protocol with fixed-size headers and typed fields. Use DataView for reading and writing, or a serialization library like MessagePack for structured data that still needs some flexibility.

Key Takeaway

Multiplayer networking follows the same patterns regardless of rendering technology. Use an authoritative server for game integrity, client-side prediction for responsive local movement, entity interpolation for smooth remote rendering, and binary delta compression to keep bandwidth manageable. The JavaScript ecosystem makes it practical to share game logic between client and server.