Guide · tested October 2026

How to make a multiplayer browser game

A multiplayer browser game has three parts: the page each player opens, a server that every page connects to, and a live connection between them. This guide builds all three in about 100 lines of JavaScript. You can run it today, and I load-tested it with 200 bot players so you know what it costs before you host it.

If you built your game with an AI coding tool, this is also the shape to ask it for. Paste the "rules" section below into your prompt.

How it works

Step 1: the server

Make a folder, save this as server.js, and install the one dependency with npm install ws. Add "type": "module" to your package.json.

// Minimal authoritative multiplayer server: serves the page and runs the game loop.
import http from "node:http";
import { readFile } from "node:fs/promises";
import { WebSocketServer } from "ws";

const PORT = process.env.PORT || 3000;
const TICK_MS = 50;          // 20 updates per second
const SPEED = 200;           // pixels per second
const W = 800, H = 600;

const server = http.createServer(async (req, res) => {
  if (req.url === "/" || req.url === "/index.html") {
    res.writeHead(200, { "content-type": "text/html" });
    res.end(await readFile(new URL("./public/index.html", import.meta.url)));
  } else {
    res.writeHead(404).end();
  }
});

const wss = new WebSocketServer({ server });
const players = new Map(); // id -> { x, y, color, input }
let nextId = 1;

wss.on("connection", (ws) => {
  const id = nextId++;
  const player = {
    x: Math.random() * W, y: Math.random() * H,
    color: `hsl(${Math.floor(Math.random() * 360)} 70% 55%)`,
    input: { up: false, down: false, left: false, right: false },
  };
  players.set(id, player);
  ws.send(JSON.stringify({ type: "welcome", id }));

  ws.on("message", (data) => {
    let msg;
    try { msg = JSON.parse(data); } catch { return; }
    if (msg.type === "input") player.input = msg.input;          // clients send intent, never positions
    if (msg.type === "ping") ws.send(JSON.stringify({ type: "pong", t: msg.t }));
  });
  ws.on("close", () => players.delete(id));
});

setInterval(() => {
  const dt = TICK_MS / 1000;
  for (const p of players.values()) {
    const { up, down, left, right } = p.input;
    p.x = Math.max(0, Math.min(W, p.x + ((right ? 1 : 0) - (left ? 1 : 0)) * SPEED * dt));
    p.y = Math.max(0, Math.min(H, p.y + ((down ? 1 : 0) - (up ? 1 : 0)) * SPEED * dt));
  }
  const state = JSON.stringify({
    type: "state",
    players: [...players].map(([id, p]) => ({ id, x: Math.round(p.x), y: Math.round(p.y), color: p.color })),
  });
  for (const client of wss.clients) if (client.readyState === 1) client.send(state);
}, TICK_MS);

server.listen(PORT, () => console.log(`listening on http://localhost:${PORT}`));

Step 2: the page

Save this as public/index.html. It opens the connection, sends key presses, and draws circles. Your own player has a white ring.

<!doctype html><html><head><meta charset="utf-8"><title>Multiplayer demo</title>
<style>body{margin:0;background:#111;color:#ddd;font:14px system-ui}canvas{display:block;margin:12px auto;background:#1c1f26}p{text-align:center}</style>
</head><body>
<canvas id="c" width="800" height="600"></canvas>
<p>Arrow keys or WASD to move. Open this page in a second tab to see another player. <span id="ping"></span></p>
<script>
const proto = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${proto}://${location.host}`);
const ctx = document.getElementById("c").getContext("2d");
let myId = null, players = [];
const input = { up: false, down: false, left: false, right: false };
const keys = { ArrowUp: "up", w: "up", ArrowDown: "down", s: "down", ArrowLeft: "left", a: "left", ArrowRight: "right", d: "right" };

function setKey(e, down) {
  const k = keys[e.key];
  if (!k || input[k] === down) return;
  input[k] = down;
  if (ws.readyState === 1) ws.send(JSON.stringify({ type: "input", input }));
}
addEventListener("keydown", (e) => setKey(e, true));
addEventListener("keyup", (e) => setKey(e, false));

ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  if (msg.type === "welcome") myId = msg.id;
  if (msg.type === "state") players = msg.players;
  if (msg.type === "pong") document.getElementById("ping").textContent = `Ping: ${Math.round(performance.now() - msg.t)} ms`;
};
setInterval(() => ws.readyState === 1 && ws.send(JSON.stringify({ type: "ping", t: performance.now() })), 2000);

(function draw() {
  ctx.clearRect(0, 0, 800, 600);
  for (const p of players) {
    ctx.fillStyle = p.color;
    ctx.beginPath(); ctx.arc(p.x, p.y, 14, 0, Math.PI * 2); ctx.fill();
    if (p.id === myId) { ctx.strokeStyle = "#fff"; ctx.lineWidth = 2; ctx.stroke(); }
  }
  requestAnimationFrame(draw);
})();
</script></body></html>

Step 3: run it

node server.js
# open http://localhost:3000 in two browser tabs and move with the arrow keys

Both tabs show both players moving. To play with someone on another computer, the server has to be somewhere they can reach, which is the hosting part below.

What I measured

I ran this exact server on a desktop PC (16 cores, Node.js 26) and connected bot clients that all hold an arrow key and ping the server once a second. The bots ran on the same machine, so ping here is server processing time, not internet latency. Add your players' real network delay on top.

PlayersServer memoryState message sizePing p50 / p95Total upload from server
269 MB132 bytes0.5 / 1.3 ms0.04 Mbit/s
5070 MB2.6 KB2.7 / 6.5 ms21 Mbit/s
20080 MB10.7 KB1.8 / 12 ms340 Mbit/s

The lesson: bandwidth runs out long before CPU or memory. Every player receives every other player's position 20 times a second, so upload grows with the square of the player count. Doubling the players roughly quadruples the traffic. A 200-player room needs more upload than most cheap servers include.

Rough rule for this design: each player downloads about 55 bytes × players × 20 per second. With 8 players in a room that is about 9 KB/s per player, or roughly 32 MB per player per hour of play.

When you outgrow it

For a game with friends or small rooms, the code above is enough. Before you have hundreds of players in one place, cut the bandwidth:

Frameworks like Colyseus give you rooms, state syncing and deltas out of the box. You don't need one to start, but it saves writing these yourself.

Rules to give your AI coding tool

Make this game multiplayer with a Node.js WebSocket server (the "ws" package).
- The server is authoritative: it holds all game state and runs a fixed tick (20/s).
- Clients send only input (keys pressed), never positions or scores.
- The server broadcasts state to every client in the same room after each tick.
- Players are grouped into rooms of at most 8.
- Use wss:// when the page is served over https.

Where to host it

Your server needs three things: it must keep running all the time, it must allow long-lived WebSocket connections, and it should sit behind HTTPS. That last one matters because a page loaded over https:// cannot open a plain ws:// connection. The browser blocks it, so you need wss://.

That rules out hosting that only runs code per request or goes to sleep when idle. A small virtual server (VPS) running Node.js behind a reverse proxy like Caddy works, and so do game-specific services that run the server for you.

Get the code

All files from this guide, including the load-test script: server.js, public/index.html, test-clients.js.