METABYTE
Back to articles

Best Backend for a Realtime Multiplayer Browser Game: A Pragmatic Guide

Choosing the best backend for a realtime browser game depends on your game’s tempo, cheat tolerance, and budget. Here’s how to pick the right protocol, framework, and architecture—without painting yourself into a scaling corner.

19 mai 202615 min readAI research draft
Best Backend for a Realtime Multiplayer Browser Game: A Pragmatic Guide

If you pick the wrong backend for a realtime multiplayer browser game, you won’t just lose a week—you’ll lose players to jitter, rubber-banding, or exploiters. This article is about making the right call up front so you can ship fast and scale without rewriting your netcode under duress.

If you want the short version: pick the backend by game tempo. Turn-based or light realtime? WebSocket with an authoritative Node/Go server is plenty. Mid-to-fast action? Use an authoritative server with WebRTC DataChannels (or WebSocket with careful prediction/interpolation). At scale, shard by room, keep state in-memory, and use Redis/NATS for coordination—not for per-tick state.

Decide by game tempo and trust model

Different multiplayer patterns want different protocols and servers. In our experience as builders, the first fork is tempo (how often state changes) and trust model (are clients authoritative?).

  • Turn-based or low-frequency updates (chess, card battlers, async tactics):
    • Use WebSocket on a simple authoritative server. Socket.IO/ws/Phoenix Channels all work. Latency sensitivity is low; reliability is key.
  • Casual realtime arena or co-op (2D brawlers, co-op builders, social worlds):
    • Start with WebSocket, 10–20 Hz snapshots, client-side interpolation. For larger rooms or voice/video add WebRTC (DataChannels for state; SFU for media). Colyseus or Nakama speed you up.
  • Fast-twitch action with physics (twin-stick, platform fighters, small arena shooters):
    • Prefer WebRTC DataChannels (QUIC) to avoid TCP head-of-line blocking, or use WebSocket with strict delta compression and lag compensation. Authoritative server required to curb cheats.
  • Massive social lobbies or MMO-lite (hundreds in a shard):
    • Partition scenes spatially, send area-of-interest updates, and pick an event-driven stack (Elixir/Phoenix or Go). Expect to write custom interest management.

If you’re still picking a rendering engine, we compared them here: Three.js vs Babylon.js vs PixiJS.

Protocols: WebSocket vs WebRTC DataChannel vs WebTransport

Here’s how the wire choices shake out for browser games:

TransportLatency profileReliabilityBrowser supportServer complexityBest for
WebSocket (TCP)Stable but head-of-line blocking under lossReliable, in-orderUniversalLow (Node/Go/Phoenix easy)Turn-based, casual realtime, authoritative rooms up to ~50 players
WebRTC DataChannel (QUIC/SCTP over DTLS/SRTP)Low; resilient to lossConfigurable; ordered/unorderedVery good, requires STUN/TURNMedium/High (ice, nat, turn)Fast action rooms (2–16 players), p2p fallback, media integration
WebTransport (QUIC)Low; streams/datagramsStreams/datagramsEmerging (production in Chrome; server options limited)Medium/HighForward-looking action games if you control server+client versions

Notes:

  • WebSocket is the default for simplicity. You get TLS termination on any CDN/proxy and a stable dev story.
  • DataChannels shine when you need unordered, partially reliable messages. The cost: ICE negotiation, TURN expenses at scale, and more moving parts.
  • WebTransport is promising but, as of our production patterns, tooling is thinner and server support is uneven. Use when you’re comfortable owning more infra and testing hard.

Frameworks you’ll actually ship with

Common production patterns we see:

  • Colyseus (Node.js): Fast to prototype, opinionated room lifecycle, schema sync. Great for 2D/3D casual realtime. You still write authoritative logic.
  • Nakama (Go, by Heroic Labs): Built-in matchmaking, storage, RPCs, realtime, authoritative Lua/Go/TypeScript logic. Good if you want batteries included and a strong admin surface.
  • Phoenix Channels (Elixir): Concurrency monster, great for chat, lobbies, and lots of small rooms. Presence is excellent. You roll your game loop.
  • Go custom (Gorilla/websocket or nhooyr.io/websocket, plus Pion for WebRTC): Efficient, predictable GC, great for server-side simulation.
  • Cloudflare Workers + Durable Objects: Edge-affined rooms are compelling for casual games. Not ideal for heavy physics sims but fantastic for low-latency presence and small-room state.

Avoid overfitting the backend to a framework demo. Pick based on your loop: who simulates state, how often, and how large are messages/rooms.

Reference architecture: authoritative room servers, sharded by room

This is our default for most browser realtime titles:

  • Client: WebGL (Three.js/Babylon.js/PixiJS), input sampling at 60 Hz, send inputs at 20–30 Hz.
  • Gateway: HTTP(S) + WebSocket/WebRTC signaling endpoint. Authenticate via OAuth/JWT. Sticky load balancing by room.
  • Room servers (authoritative):
    • In-memory state per room; simulate at 20–30 ticks/sec.
    • Broadcast deltas to subscribed players.
    • Discard client-reported positions; accept only inputs; apply reconciliation.
  • Matchmaking/coordination: Redis or NATS to announce room capacity and IP/port; never send per-tick state through Redis.
  • Persistence: Postgres for accounts/inventory/progression; append match results only after game end.
  • Observability: Prometheus/OpenTelemetry, Grafana, Sentry. Track tick duration p95, outbound bytes/player, room churn, GC pauses.
  • Optional media: SFU (Janus/mediasoup) for voice; don’t multiplex heavy media in the same process as the game loop.
  • Optional P2P: WebRTC DataChannels for small rooms with server-authoritative arbiter. TURN budget accordingly.
  • CDN: Cloudflare/Akamai for assets; don’t serve game textures from your room servers.

Room affinity is non-negotiable: keep all players in a room on the same process to avoid cross-node chatter during ticks.

Minimal Node WebSocket authoritative loop (TypeScript)

This is intentionally compact to illustrate the structure, not production-hardened. It shows how to keep per-room state in-memory, simulate, and broadcast deltas.

// package.json deps: ws, uWebSockets.js (optional), msgpack-lite
import { WebSocketServer, WebSocket } from 'ws';
import msgpack from 'msgpack-lite';

const TICK_MS = 50; // 20 Hz
const MAX_PLAYERS_PER_ROOM = 16;

type Vec2 = { x: number; y: number };
type Player = { id: string; pos: Vec2; vel: Vec2; lastSeq: number };

type InputMsg = { t: 'input'; seq: number; ax: number; ay: number };

type SnapshotMsg = { t: 'snap'; ts: number; players: Array<{ id: string; x: number; y: number }> };

class Room {
  id: string;
  sockets = new Map<string, WebSocket>();
  players = new Map<string, Player>();
  lastSnap: Uint8Array | null = null;

  constructor(id: string) { this.id = id; }

  join(id: string, ws: WebSocket) {
    if (this.sockets.size >= MAX_PLAYERS_PER_ROOM) throw new Error('room full');
    this.sockets.set(id, ws);
    this.players.set(id, { id, pos: { x: 0, y: 0 }, vel: { x: 0, y: 0 }, lastSeq: 0 });
  }

  leave(id: string) {
    this.sockets.delete(id);
    this.players.delete(id);
  }

  onInput(id: string, msg: InputMsg) {
    const p = this.players.get(id);
    if (!p) return;
    if (msg.seq <= p.lastSeq) return; // drop old/replayed
    p.lastSeq = msg.seq;
    // simple accel integration; clamp for sanity
    p.vel.x = Math.max(-1, Math.min(1, msg.ax));
    p.vel.y = Math.max(-1, Math.min(1, msg.ay));
  }

  tick(dt: number) {
    // simulate
    for (const p of this.players.values()) {
      p.pos.x += p.vel.x * dt * 0.005;
      p.pos.y += p.vel.y * dt * 0.005;
    }
    // encode minimal snapshot (delta packing omitted for brevity)
    const snap: SnapshotMsg = {
      t: 'snap',
      ts: Date.now(),
      players: Array.from(this.players.values()).map(p => ({ id: p.id, x: p.pos.x, y: p.pos.y }))
    };
    this.lastSnap = msgpack.encode(snap);
    // broadcast with backpressure awareness
    for (const [id, ws] of this.sockets) {
      if (ws.readyState === ws.OPEN && ws.bufferedAmount < 64 * 1024) {
        ws.send(this.lastSnap, { binary: true });
      }
    }
  }
}

const rooms = new Map<string, Room>();
const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws, req) => {
  const url = new URL(req.url || '', 'http://x');
  const roomId = url.searchParams.get('room') || 'lobby';
  const playerId = crypto.randomUUID();

  let room = rooms.get(roomId);
  if (!room) { room = new Room(roomId); rooms.set(roomId, room); }
  room.join(playerId, ws);

  ws.on('message', (data) => {
    try {
      const msg = msgpack.decode(new Uint8Array(data as Buffer)) as InputMsg;
      if (msg.t === 'input') room!.onInput(playerId, msg);
    } catch (e) { /* ignore malformed */ }
  });

  ws.on('close', () => room!.leave(playerId));
});

let last = Date.now();
setInterval(() => {
  const now = Date.now();
  const dt = now - last; last = now;
  for (const room of rooms.values()) room.tick(dt);
}, TICK_MS);

If you need unordered or lossy messages, move to WebRTC DataChannels and keep the same loop structure. The authoritative logic barely changes; you only swap transport and signaling.

Sharding and service boundaries

  • One process = many rooms, but cap max players per room and total connections per process to keep p95 tick under budget.
  • Run a lightweight coordinator that:
    • Assigns new players to a room with capacity.
    • Emits a sticky routing token (roomId -> server address).
    • Exposes health and metrics.
  • Don’t centralize per-tick state in Redis or Postgres. Those are for coordination and results, not the hot path.

We often start with processes pinned to regions (e.g., us-east, eu-west) and expand when telemetry shows that adding an edge region reduces p95 by >20 ms for a meaningful cohort. Premature multi-region adds complexity without player-visible wins.

WebRTC: when and how

Use DataChannels when your game is fast-twitch and you care about:

  • Unordered/partial reliability (no TCP head-of-line stalls when one packet is lost).
  • Media integration (voice/video) alongside state channels.
  • P2P options to reduce server egress for tiny matches (with a server arbiter for authority).

What it costs:

  • STUN/TURN infra: budget for TURN egress because many players are behind symmetric NATs.
  • More complex signaling. You’ll run a signaling service (often over WebSocket/HTTP) to exchange SDP/ICE.
  • Debuggability is harder. Build tooling early: connection stats, RTT, loss, retransmits, ICE state.

If you go server-authoritative over WebRTC, consider Go + Pion or Rust + Quiche for server-side QUIC/WebRTC. Node has options, but GC pauses at 60+ rooms per process can bite you under pressure unless you’re very disciplined with allocations.

State sync strategies that actually work in browsers

Pick one and commit:

  • Snapshot + interpolation (authoritative): Server broadcasts position/rotation at 10–30 Hz. Clients interpolate and correct. Add input prediction + reconciliation to hide latency.
  • Input lockstep (peer or server lockstep): All players send inputs for tick N; simulation advances when everyone has N. Great determinism; brittle under jitter and reconnects.
  • Server RPC events: For non-physical worlds (co-op builder, social), broadcast discrete events (spawn, open-door) and let clients lerp visuals.

Baseline budgets we aim for:

  • Target < 1 KB per player per second outbound for casual realtime; < 5 KB/s for action. Use binary, pack ints, delta-compress.
  • 20–30 Hz tick is a sweet spot. The player’s browser still renders at 60 FPS with interpolation, and your server has headroom.
  • Cap room sizes: 8–16 for action; 20–50 for casual; hundreds only with strict area-of-interest and event filtering.

What breaks in production

This is where most teams pay the tax.

  • Head-of-line blocking (WebSocket/TCP):
    • Symptom: Everyone rubber-bands when a single player on poor Wi‑Fi stalls the stream.
    • Mitigation: Keep messages small and frequent. Split state into channels (critical vs cosmetic). Consider unordered channels (WebRTC).
  • GC pauses and object churn:
    • Symptom: p95 tick spikes with players joining/leaving; heap grows.
    • Mitigation: Preallocate buffers; reuse arrays; avoid JSON; use binary codecs; prefer structs of arrays. Profile with flamegraphs.
  • N+1 broadcast loops:
    • Symptom: CPU burns in per-player send loops.
    • Mitigation: Build snapshots once per tick; share byte buffers; apply backpressure (bufferedAmount check).
  • TURN egress bill shock:
    • Symptom: Costs jump after launch weekend.
    • Mitigation: Prefer server-relayed authoritative paths for state; reserve TURN for P2P or media only; set bitrate caps.
  • DDoS/abuse:
    • Symptom: Gateways saturate, matchmaking flaps.
    • Mitigation: Put a CDN/WAF in front of HTTP; rate limit signaling; validate tokens at the edge; isolate game loop ports from the public where possible.
  • Version skew:
    • Symptom: Silent desyncs after a hotfix.
    • Mitigation: Versioned message schemas; server rejects mismatched clients with a prompt to refresh; feature flags by build ID.
  • Cross-region stickiness:
    • Symptom: Friends end up on different regions with 150 ms ping between them.
    • Mitigation: Region-as-a-choice UX; friend group pinning; simple heuristics (majority region wins).

One final note: logs are not telemetry. Emit per-room stats every tick window and build alerts for “rooms above 80% tick budget for >60s.” Your future self will thank you.

Business reality: cost, team, and when to buy vs build

You can ship a solid realtime browser backend without setting money on fire, but trade-offs matter.

  • Prototype/beta (hundreds of CCU):
    • Stack: WebSocket + Colyseus/Nakama or custom Node/Go; Postgres; Redis; single region.
    • Infra budget: roughly low hundreds USD/month (compute + DB + CDN). TURN minimal or none.
    • Team: 1–2 engineers can manage netcode + gameplay + ops if scope is controlled.
  • Early scale (few thousands CCU):
    • Stack: Add autoscaling room servers; put metrics in place; introduce a job queue for match results; optional SFU for voice.
    • Infra budget: low thousands/month, mostly compute and egress; TURN if using WebRTC.
    • Team: Dedicated owner for netcode/infra; QA for latency edge cases.
  • Established live ops (10k+ CCU):
    • Stack: Multi-region; coordinator service; stress-tested matchmaker; chaos testing; canary deploys; feature flags.
    • Infra budget: mid five figures/month depending on egress, TURN, and regions. This is where optimization pays back fast.
    • Team: Netcode lead, SRE/on-call rotation, analytics.

Buy vs build:

  • Buy Nakama/managed or use Cloudflare Durable Objects if you want speed and standardized operations. Your lock-in is acceptable if your game loop fits their model.
  • Build custom (Go/Elixir/Rust) if your simulation is unique, room sizes are atypical, or you need precise control over wire formats and CPU.

Hidden expensive mistakes:

  • Pushing per-tick state through Redis/Kafka because it “worked in staging.” It will bottleneck and add latency.
  • Multiplexing voice/video and game loop in the same process.
  • Skipping observability until after launch.
  • Letting client positions be authoritative (you invited cheaters). Server should validate and simulate.

If you plan to expand beyond web to native storefronts, we wrote about the production curve here: Ship a Steam game from a web studio background.

Suggested stacks by game type

  • Turn-based/async tactics:
    • Protocol: WebSocket
    • Backend: Phoenix Channels or Node + ws/Socket.IO
    • DB: Postgres; Redis for matchmaking queues
    • Notes: Focus on reliability, replays, and anti-cheat on move validation.
  • Casual realtime (co-op build, arcade arena):
    • Protocol: WebSocket (20 Hz); optionally DataChannel for cosmetic effects
    • Backend: Colyseus or Nakama; Node/Go if you need custom loops
    • DB: Postgres; Redis for presence
    • Notes: Snapshot + interpolation; keep payloads tight with binary encoders.
  • Fast action (8–16 players):
    • Protocol: WebRTC DataChannels (unordered, partially reliable)
    • Backend: Go + Pion; Node only if you’re aggressive with allocations
    • DB: Minimal during match; write results at end
    • Notes: Lag compensation; authoritative hit detection; cheat-resistant inputs.
  • Social hub/MMO-lite (100+ visible):
    • Protocol: WebSocket
    • Backend: Elixir/Phoenix; Cloudflare Durable Objects for edge rooms and presence
    • DB: Postgres; Redis/NATS for pub/sub
    • Notes: Area-of-interest; rate-limit chat and emotes; shard by space.

Tuning playbook: latency and bandwidth

  • Set a tick budget (e.g., 20 ms total per tick at 20 Hz) and measure: sim time, encode time, send time.
  • Use binary everywhere. JSON is for APIs, not your hot path. Protobuf, FlatBuffers, msgpack, or custom bit-packing.
  • Apply delta compression against last acknowledged state per client when feasible. For small rooms, shared snapshot buffers are fine.
  • Backpressure: never let bufferedAmount grow unbounded. Drop cosmetic updates if the socket is congested.
  • Security: sign tokens; reject unknown message types; cap message sizes; sanitize inputs; use rate limits per connection.
  • Test on lossy/wifi 2.4GHz with 5% packet loss and 120 ms latency. Your happy-path LAN test is lying to you (politely).

FAQ

Q: Is WebRTC always better than WebSocket for realtime games? A: No. WebRTC is better when unordered/partially-reliable delivery helps your gameplay and you can afford the extra infra (TURN) and complexity. WebSocket remains excellent for casual realtime and anything not ultra twitchy.

Q: Can I run everything on serverless/edge? A: For casual realtime and lobbies, yes—Cloudflare Durable Objects or similar can host room logic close to players. For heavy physics at 20–60 Hz, long-lived connections and CPU predictability often favor dedicated VMs/containers.

Q: How big can a single room be? A: For action games, keep rooms under 16–20 players unless you have strict interest management and very compact messages. Social spaces can host hundreds with area-of-interest and event filtering.

Q: Do I need authoritative servers for all games? A: If you care about fairness or economy integrity, yes. Turn-based can validate moves on the server; realtime should simulate on the server and treat client input as suggestions, not truth.

Q: What database is best for realtime state? A: None. Keep per-tick state in-memory in the room server. Use Postgres for accounts/progression and Redis/NATS for matchmaking and presence.

Q: How do I estimate server capacity? A: Benchmark your loop with realistic messages. As a rough guide, a well-optimized room server can host many small rooms per vCPU. Watch p95 tick time; when it creeps toward your tick interval, reduce rooms per process or scale out.

Key takeaways

  • Choose transport by game tempo: WebSocket for most, WebRTC DataChannels for fast-twitch.
  • Keep authoritative state in-memory and shard by room; use Redis/NATS for coordination only.
  • Aim for 20–30 Hz ticks, binary messages, and <1–5 KB/s per player outbound.
  • Build observability early: tick budgets, GC, per-room throughput, and backpressure.
  • TURN egress and TCP head-of-line are common hidden costs; plan around them.
  • Don’t mix media and simulation in the same process; isolate to keep ticks predictable.

If you’re building a realtime multiplayer browser game and want a backend that ships fast and survives launch, MTBYTE can design and implement the stack end-to-end. Talk to us at /contact.

NEXT STEP

Liked the approach?

We apply the same principles to client projects: AI, automation, products that don't die after launch.