Real-Time Multiplayer with WebSockets

Updated June 2026
WebSockets are the foundation of most real-time multiplayer web games. They provide a persistent, full-duplex TCP connection between the browser and server, allowing both sides to send messages at any time without the overhead of HTTP request/response cycles. This guide walks through building a multiplayer game connection from the ground up, from server setup to binary protocols and reconnection logic.

WebSockets replaced the older techniques of HTTP long-polling and Server-Sent Events for real-time game communication because they offer lower latency, bidirectional messaging, and efficient binary data transfer. Every modern browser supports the WebSocket API, making it the most reliable transport option for multiplayer web games that need to work across all devices and networks.

Set Up the WebSocket Server

The server side of a WebSocket game connection runs on Node.js (or any backend language with WebSocket support). The most common Node.js libraries are ws (a lightweight, fast WebSocket implementation) and uWebSockets.js (a high-performance C++ backed library with a JavaScript API). For most web games, ws is sufficient and easier to work with.

Install the library with npm install ws, then create a WebSocket server that listens on a port. The server should handle three core events for each connection: connection (a new player connects), message (a player sends data), and close (a player disconnects). Each connected socket represents one player, and you should assign a unique identifier to each connection immediately upon connection.

For production, the WebSocket server should run behind a reverse proxy (nginx, Caddy, or a cloud load balancer) that terminates TLS. Browsers require wss:// (WebSocket Secure) for any page served over HTTPS, which is every page in modern web hosting. The reverse proxy handles the TLS handshake and forwards the raw WebSocket connection to your Node.js process.

Establish Client Connections

On the client side, the browser's built-in WebSocket API handles the connection. Create a new connection with new WebSocket("wss://your-server.com/game"). The API provides four event handlers: onopen fires when the connection is established, onmessage fires when the server sends data, onclose fires when the connection ends, and onerror fires on connection errors.

Set the binaryType property to "arraybuffer" immediately after creating the connection. By default, WebSocket binary messages arrive as Blob objects, which require asynchronous reading. ArrayBuffer is synchronous and much faster for game data parsing. Every game WebSocket connection should use ArrayBuffer mode.

Implement a connection state machine on the client that tracks whether the socket is connecting, connected, disconnected, or reconnecting. This state drives the game's UI (showing connection status to the player) and its network behavior (queuing messages during reconnection, discarding stale state after a long disconnection).

Define a Binary Message Protocol

JSON is convenient for prototyping but unacceptable for production multiplayer games. A JSON position update like {"type":"pos","id":42,"x":123.456,"y":789.012,"angle":1.57} is 58 bytes. The same data in binary is 17 bytes: 1 byte for message type, 2 bytes for player ID (uint16), 4 bytes each for x, y, and angle (float32). At 20 updates per second for 20 players, the binary format saves roughly 16 KB per second per room. Over hundreds of rooms, this difference is substantial.

Use JavaScript's DataView class to read and write binary data. Create a write function for each message type that allocates an ArrayBuffer of the exact size needed, writes each field at the correct byte offset using methods like setUint8, setFloat32, and setUint16, and returns the buffer. Create a corresponding read function that takes an ArrayBuffer, reads the message type from the first byte, and dispatches to the appropriate parser.

Keep your protocol versioned. Include a version byte in the connection handshake so the server can reject clients running incompatible protocol versions. As your game evolves and message formats change, incrementing the protocol version prevents corrupted data from crashing either side.

Implement a Server Game Loop

The server needs a fixed-rate tick loop that processes all pending player inputs, advances the game simulation by one tick, and sends the updated state to all connected clients. Use setInterval for a simple implementation. A 50-millisecond interval gives you 20 ticks per second, which is adequate for most web games. For faster-paced games, use 33 milliseconds (30 ticks per second) or even 16 milliseconds (roughly 60 ticks per second), though higher tick rates increase CPU and bandwidth costs proportionally.

Process all received inputs at the start of each tick, not as they arrive. Buffer incoming messages in an array and process them all at once during the tick. This ensures deterministic behavior regardless of message arrival timing and prevents mid-tick state corruption. After processing inputs, run the game simulation (physics, collision detection, game rules), then serialize the updated state and broadcast it to all players in the room.

Delta compression is essential at this stage. Track which parts of the game state changed since the last broadcast and only send those changes. Many entity properties (health, inventory, team) change infrequently, so sending only position and rotation deltas for entities that moved reduces bandwidth by 70 to 90 percent compared to full state broadcasts.

Build Room Management

Organize your game server around rooms. Each room is an independent game instance with its own player list, game state, and tick loop. When a player connects, they either join an existing room (matched by game mode, skill level, or explicit room code) or a new room is created for them.

Each room should have a well-defined lifecycle. It starts in a "waiting" state while players join. Once enough players are present (or a countdown expires), it transitions to "playing." When the game ends (by win condition, timeout, or all players leaving), it enters a "finished" state where final results are calculated and sent. After a brief results period, the room is destroyed and its resources freed.

Limit the number of rooms per server process based on load testing. Monitor CPU and memory usage under realistic conditions (not just idle connections, but active gameplay with physics and state updates). A typical Node.js process can handle 20 to 50 rooms with 4 to 8 players each at 20 ticks per second, but this varies enormously based on game complexity.

Add Reconnection Support

WebSocket connections drop frequently in real-world conditions. Mobile players switch between WiFi and cellular, laptop players close and reopen their browser, and network hiccups cause momentary disconnections. Without reconnection support, every dropped connection means the player loses their game in progress.

Implement heartbeat monitoring using WebSocket ping/pong frames. The server sends a ping every 10 to 15 seconds. If no pong is received within 5 seconds, the server considers the connection dead. On the client side, if no message is received from the server for 10 seconds, the client should assume the connection is dead and begin reconnection attempts.

When a player disconnects, do not immediately remove them from the room. Mark them as disconnected and start a grace period timer (30 seconds to 2 minutes depending on your game). During this period, the player's slot is held. If they reconnect and provide their session token, they are reattached to the room and receive a full state snapshot to rebuild their local game state. If the grace period expires, remove the player permanently and broadcast the departure to remaining players.

On the client, implement exponential backoff for reconnection attempts. Try to reconnect immediately, then wait 1 second, then 2, then 4, up to a maximum of 30 seconds between attempts. Include jitter (random variation in the delay) to prevent all disconnected clients from reconnecting at exactly the same moment and overwhelming the server.

Key Takeaway

WebSockets provide the simplest and most reliable path to real-time multiplayer in the browser. Use binary messages from the start, run a fixed-rate server tick loop, organize players into rooms, and implement reconnection with grace periods. These fundamentals work whether you build on raw WebSockets or adopt a framework like Colyseus later.