METABYTE
Inapoi la articole

Best backend pentru un joc multiplayer realtime in browser

Alegi backendul pentru un joc multiplayer realtime in browser? Comparatie pragmatica intre WebSocket, WebRTC, Nakama, Colyseus, Phoenix, Rust. Arhitecturi, costuri, capcane.

19 mai 202616 min de cititAI-research draft
Best backend pentru un joc multiplayer realtime in browser

Daca alegi gresit backendul pentru un joc multiplayer realtime in browser, platesti dublu: o data in lag si rage-quit, a doua oara in facturi cloud care cresc mai repede decat MMR-ul cheaterilor.

In 2-3 fraze: pentru jocuri cu actiune rapida si 1–10k CCU, un server autoritativ pe WebSocket este traseul cel mai sigur: Node.js cu uWebSockets.js sau Go (Nakama) daca tinta ta e >5k CCU. WebRTC DataChannels e util pentru P2P in camere mici, dar cerintele TURN/LB si anti-cheat il fac mai scump. Pentru latente mici si scalare eleganta, Elixir Phoenix Channels sau Rust (Tokio/Actix) aduc headroom, cu cost de dezvoltare mai mare.

Ce inseamna "realtime" in browser: bugete si protocoale

  • Buget de latenta: pentru un shooter/arena, 50–120 ms RTT simtit; pentru co-op casual, 150–250 ms e acceptabil.
  • Tick rate: 20–60 Hz pe server. 60 Hz arata bine pe hartie, dar dubleaza CPU-ul fata de 30 Hz. In productie, 20–30 Hz cu client-side prediction si snapshot interpolation este punctul pragmatic.
  • Transport:
    • WebSocket (TCP) este standard, matur, usor de operat, merge prin CDN/LB si firewall-uri. Sufera de head-of-line blocking (HOL), dar cu tinte sub ~64 KB/s per client si pachete mici, este suficient.
    • WebRTC DataChannels (SCTP peste DTLS) aduce partial reliability si unordered delivery. In practica: STUN/TURN, ICE, tricky in server-hosted authoritative, dar excelent pentru P2P 1v1/party mic.
    • WebTransport/QUIC abia se stabilizeaza. Pro-miscare pentru viitor, dar astazi stackul de productie este subtire. Il folosim doar cand chiar avem nevoie de datagram reliability partiala si control fin.

Un joc browser modern (Three.js/Babylon.js/WebGL) are oricum un strat de netcode care mascheaza jitterul: client-side prediction, reconciliere, lag compensation pe server. Daca partea vizuala te intereseaza, vezi comparatia noastra despre randare in Three.js vs Babylon.js vs PixiJS.

Modele de sincronizare a jocului

  • State sync + snapshot interpolation: serverul trimite periodic un snapshot compact (delta sau full) al starii relevante per jucator. Clientul interpoleaza intre snapshots. Cel mai comun pentru arcade/shooter/arena.
  • Comenzi + reconciliere: clientul trimite input cu inputSeq, simuleaza local; serverul este autoritativ si trimite corectii, clientul re-simuleaza. Reduce perceptia lagului.
  • Lockstep determinist: ideal pentru RTS-uri; toti trimit aceleasi comenzi pe tickuri, simularile sunt deterministe. Fragil la divergente floating-point si la desync; greu in JS pur fara efort.
  • Interest management: la >20 actori, nu trimiti tot catre toti. Filtre pe raza, vizibilitate, echipa; micro-spatii (grid/quadtree). Aici se castiga 50–80% din trafic.

Arhitecturi recomandate (concrete, cu tooluri)

1) "Start corect" — Node.js (uWebSockets.js) + Redis + Postgres

Cand: 1–5k CCU, camere de 2–20 jucatori, tick 20–30 Hz, gameplay CPU-light.

  • Ingress: Cloudflare proxied (WS), HTTP API pentru login/cont. Optional Argo Tunnel pentru simplificare.
  • Load balancing: NLB/ALB (AWS) sau HAProxy/Envoy cu sticky pe roomId hash.
  • Gateway: Node.js + uWebSockets.js pentru performanta pe conexiuni concurente; ws merge, dar uWS scade overhead-ul.
  • Game servers: procese Node izolate per shard, ruleaza camere (rooms). Autoritative, tin stare in memorie.
  • Redis: matchmaker, room directory, pub/sub pentru spectate sau notifs cross-room.
  • Postgres (RDS/GKE + Cloud SQL): conturi, progres, economie. Migrare cu Prisma/TypeORM.
  • Telemetrie: Prometheus + Grafana; Sentry pentru erori; Loki/ELK pentru loguri.

Flux:

  • Client -> HTTP login (JWT) -> Gateway WS -> assign room (Redis) -> join -> bucla de joc -> snapshots la 50 ms.
  • Persistenta la final de meci sau checkpoint (nu la fiecare kill, altfel Postgres te va uri).

Un schelet minimal pentru un server autoritativ cu predict + reconciliere:

import uWS from 'uWebSockets.js';
import { createClient } from 'redis';

const PORT = Number(process.env.PORT || 9001);
const TICK_MS = 50; // 20 Hz

// Stare in memorie, shard per proces
type Player = { id: string; x: number; y: number; vx: number; vy: number; lastSeq: number };
const rooms: Record<string, { players: Map<number, Player>; tick: number }> = {};

// Inputuri queue-uite per tick
const inputQueue: Record<string, Array<{ pid: number; seq: number; dx: number; dy: number }>> = {};

function stepRoom(roomId: string) {
  const room = rooms[roomId];
  if (!room) return;
  const inputs = inputQueue[roomId] || [];
  // proceseaza inputuri in ordinea sosirii (cu clamp)
  for (const inp of inputs.splice(0, inputs.length)) {
    const p = room.players.get(inp.pid);
    if (!p) continue;
    if (inp.seq <= p.lastSeq) continue; // drop dubluri/out-of-order
    p.vx = Math.max(-1, Math.min(1, inp.dx));
    p.vy = Math.max(-1, Math.min(1, inp.dy));
    p.lastSeq = inp.seq;
  }
  // integreaza vitezele
  room.players.forEach((p) => {
    p.x += p.vx * 0.05; // 50 ms pas
    p.y += p.vy * 0.05;
  });
  room.tick++;
}

function snapshot(roomId: string) {
  const room = rooms[roomId];
  if (!room) return new Uint8Array();
  // serializare compacta (ex: ID:int, x,y,float32). Aici e JSON pt simplitate
  const state = [] as any[];
  room.players.forEach((p, id) => state.push([id, +p.x.toFixed(3), +p.y.toFixed(3)]));
  return Buffer.from(JSON.stringify({ t: room.tick, s: state }));
}

const app = uWS.App();

app.ws('/*', {
  maxPayloadLength: 16 * 1024,
  idleTimeout: 120,
  upgrade: (res, req, context) => {
    // Valideaza JWT + alege room
    const roomId = req.getQuery('room') || 'lobby';
    res.upgrade({ roomId }, req.getHeader('sec-websocket-key'), req.getHeader('sec-websocket-protocol'), req.getHeader('sec-websocket-extensions'), context);
  },
  open: (ws) => {
    const roomId = (ws as any).roomId as string;
    const room = (rooms[roomId] ||= { players: new Map(), tick: 0 });
    const pid = Math.floor(Math.random() * 1e9);
    (ws as any).pid = pid;
    room.players.set(pid, { id: String(pid), x: 0, y: 0, vx: 0, vy: 0, lastSeq: 0 });
    ws.subscribe('r:' + roomId);
    ws.send(JSON.stringify({ type: 'welcome', pid }));
  },
  message: (ws, msg, isBinary) => {
    try {
      const data = JSON.parse(Buffer.from(msg).toString());
      if (data.type === 'input') {
        const roomId = (ws as any).roomId as string;
        (inputQueue[roomId] ||= []).push({ pid: (ws as any).pid, seq: data.seq, dx: data.dx, dy: data.dy });
      }
    } catch {}
  },
  close: (ws) => {
    const roomId = (ws as any).roomId as string;
    rooms[roomId]?.players.delete((ws as any).pid);
  }
});

setInterval(() => {
  for (const roomId of Object.keys(rooms)) {
    stepRoom(roomId);
    const buf = snapshot(roomId);
    // broadcast pe topic
    app.publish('r:' + roomId, buf, true);
  }
}, TICK_MS);

app.listen(PORT, (token) => {
  if (!token) throw new Error('bind fail');
  console.log('listening on', PORT);
});

Minimal, dar il poti duce in productie cu rate-limiting pe inputuri, schema binara (FlatBuffers/Protobuf), si izolarea camerelor pe procese.

2) "Vrei scalare ordonata" — Go + Nakama + Redis + Postgres/CockroachDB

Cand: 5–50k CCU, matchmaking matur, economy, social, leaderboards. Nakama ofera RPC, storage, matchmaking, relays, colos usor de operat.

  • Ingress: Cloud LB (GCP/AWS), GRPC/WS terminate TLS.
  • Nakama: scrii logic in Go/TypeScript (Lua e suportat, dar noi mergem pe Go/TS pentru tooling). Rooms/MatchHandlers autoritative.
  • Redis pentru cache si coordonare; DB: Postgres sau CockroachDB daca vrei multi-region writes (cu costul complexitatii de latenta).
  • Observabilitate buna builtin; clusterizare simpla.

3) "Latency si resursa CPU" — Elixir Phoenix Channels + ETS/Mnesia

Cand: latente mici intra-DC, multe conexiuni concurente, trafic chat/arena. Phoenix Channels si BEAM gestioneaza sute de mii de WS cu stabilitate. Logica intensiva CPU se separa in NIF-uri sau microservicii Go/Rust.

  • PubSub intern puternic, OTP super pentru fault tolerance.
  • ETS/Mnesia pentru stare efemera; Postgres pentru persistenta.

4) "Headroom maxim" — Rust (Tokio/Actix) + WebSocket/WebTransport

Cand: tick inalt (60 Hz), simulari fizice serioase, low-latency. Cost de dezvoltare mai mare, dar latente si footprint excelente. Nu recomandat ca primul pas pentru o echipa web-only, dar o tinta buna pe termen mediu.

Un sfat pragmatic: incepe cu Node/Colyseus sau Nakama, masori, si abia apoi treci la Rust daca CPU devine limita. Inginerii apreciaza Rust, contabila apreciaza lansarea la timp.

Comparatie de trade-off (stackuri comune)

StackProContraCand il alegi
Node.js + uWebSockets.js (custom)Rapid de construit, ecosistem JS, cost mic la start, cod unic cu clientGC spikes daca blochezi event loop, performanta limitata la 10–20k conexiuni/proces, debug GCPrototip -> 1–5k CCU, camere mici, netcode simplu
Colyseus (Node)Model rooms/schema gata, sincronizare delta, matur pe WebSocketMai putin control fin, overhead vs. uWS rawJocuri casual/co-op, time-to-market scurt
Nakama (Go)Matchmaking/storage/social builtin, scalare usoara, tooluriCurbura de invatare, mai greoi pentru custom protocols5–50k CCU, set complet de feature-uri
Elixir Phoenix ChannelsConexiuni masive, OTP/rezilienta, cod expresivCPU-bound logic trebuie externalizat, hiring pool mai micChat intens + jocuri arena cu multe conexiuni
Rust (Tokio/Actix)Throughput ridicat, latenta scazuta, memorie predictibilaTimp de dezvoltare, tooling netcode DIYFizica grea, 60 Hz, CCU mare pe nod
WebRTC P2P + SFULatență peer-to-peer, cost trafic scazutTURN necesar in 10–20% cazuri, anti-cheat dificil, complexParty mic, co-op, fara economie competitiva

Scalare si topologie: camere ca unitate de planificare

  • Room-based sharding: fiecare camera ruleaza intr-un proces/thread distinct. Nu partaja starea intre camere daca nu e absolut necesar.
  • Sticky load balancing: hash pe roomId sau matchId la LB (HAProxy hash-type consistent), ca sa eviti cross-node chatter.
  • Redis ca director de camere: roomId -> node:port. Evita pub/sub flood; foloseste-l doar pentru semnalizare, nu pentru tick data.
  • Matchmaker separat: un mic serviciu care face seat allocation si MM logic (Elo, ping, preferinte). Pastreaza-l stateless, cu Redis ca queue.
  • Interest management pe server: spatial hash grid 64x64 sau quadtree; trimite doar ce conteaza pentru fiecare jucator.
  • CDN pentru assets si versiuni client; nu pune WS prin CDN generic care face buffering; Cloudflare suporta WS nativ.

Tuning de retea si cod

  • Nagle: dezactiveaza-l la TCP (TCP_NODELAY) daca biblioteca permite, pentru a reduce latency de coalescing.
  • Pachete mici, binare: Protobuf/FlatBuffers; pastreaza sub 1 KB/mesaj pentru jocuri rapide.
  • Backpressure: daca clientul nu citeste, nu umple bufferul; aplica drop la cozi vechi si trimite doar ultimul snapshot.
  • Rate limiting: 30–60 inputuri/sec/client este suficient. Dropeaza excesul si marcheaza potential abuz.
  • GC in Node: evita obiecte temporare de scurta viata in bucle; foloseste pooluri simple pentru buffers.

Persistenta: economie, progres, leaderboards

  • Postgres pentru tranzactii consistente (inventar, monede). Foloseste SERIALIZABLE doar unde conteaza; altfel READ COMMITTED.
  • CockroachDB pentru multi-region writes daca ai jucatori globali si vrei sesiuni scurte in mai multe regiuni (platesti cu latenta pe commituri). Alternativa: un Postgres/region si replici read-only + geo-sharding al jucatorilor.
  • Redis pentru cache si rate limits (scripturi Lua pentru atomicitate simpla).
  • Leaderboards: evita ORDER BY score DESC pe tabele mari in timp real. Foloseste sorted sets in Redis sau un serviciu dedicat.
  • Analytics: Redpanda/Kafka -> ClickHouse pentru telemetrie agregata la cost mic.

Anti-cheat si securitate

  • Server autoritativ: clientul trimite intentii, serverul valideaza limitele fizice si coliziunile.
  • Lag compensation: pentru proiectile, pastreaza istoricul pozitiei jucatorilor 200–300 ms si reconciliaza loviturile in trecut.
  • Semnaturi pe client build, dar nu te baza pe ele. Heuristici: rata de input, pattern-uri imposibile, server-side sanity checks.
  • Autentificare: JWT scurte (15–60 min), refresh pe HTTP; nu pune chei secrete in client.
  • DDoS: Cloudflare/LB cu rate limits, WS caps, si un circuit breaker in gateway.

Ce se strica in productie

  • Spikes de latenta din cauza GC/event loop blocking in Node: operatii sync de fs/crypto, JSON uriase. Solutie: izolati I/O grele in worker threads sau microservicii Go/Rust; binarizati protocolul.
  • HOL in TCP: cand trimiteti un snapshot mare, micile pachete de ping sunt intarziate. Solutie: fragmentati snapshoturile si oferiti canale separate (sau WebRTC unordered pentru anumite fluxuri, daca merita complexitatea).
  • Room hotspot: o camera populara aduce tot shardul in genunchi. Solutie: cap dur la marimea camerei (ex. 20), auto-split, si interes management agresiv.
  • TLS CPU la volum: terminati TLS pe LB cu accelerare, pastrati WS raw intern.
  • Loguri: 100k linii/minut omoara diskul si factura. Probati sampling si nivele stricte.
  • Metrice lipsa: fara p95/p99 RTT per room, jucatorii iti spun cum e starea serviciului. Cu metrice, vei sti tu primul.
  • NAT timeouts: keep-alive pe WS, ping/pong la 15–30s; gestionati reconectari cu reatach in aceeasi camera.

Un pic de autoironie: daca nu masori nimic, jocul tau merge perfect… la tine pe laptop.

Costuri si ROI: cat te costa si cand merita

Ordine de marime (estime orientative, nu promisiuni contractuale):

  • Node + uWS, 1–3 ingineri full-stack: MVP jucabil in 4–8 saptamani. Cost cloud: ~1–2 noduri c8.xlarge echivalent (16 vCPU/32 GB) + Redis managed + Postgres: 1.5–4k USD/luna pentru 1–3k CCU, daca optimizezi bine pachetele.
  • Colyseus: aceeasi ordine, ceva mai putin efort pe netcode. Cost similar.
  • Nakama: 2–4 luni pentru un set complet cu matchmaking, leaderboards, party, upgrade/achievements. Cost cloud: 3–6k USD/luna pentru 5–10k CCU (mai multe noduri moderate, DB managed).
  • Phoenix/Elixir: 2–3 luni pentru canalizare + rooms; performanta excelenta pe conexiuni; cost cloud scade la conexiuni idle multe.
  • Rust: 3–6 luni pentru o echipa fara experti Rust; costul operational scade, dar CAPEX dev creste.

Ce greseli sunt scumpe:

  • Over-engineering devreme (Kubernetes, service mesh, 12 microservicii) la 200 CCU. Mai bine un cluster mic, scripturi simple, apoi iteratii.
  • Fara interest management: factura de trafic si CPU explodeaza.
  • Persistenta per event: scrie pe final de match sau batch la 1–5 secunde.
  • Alegerea WebRTC doar pentru ca "este cool"; TURN si anti-cheat mananca bugetul si timpul.

Cand alegi ce: decizie rapida

  • Casual/co-op, 500–3k CCU, time-to-market: Colyseus pe Node + Redis + Postgres.
  • Arena/supra-arcade, 1–10k CCU, nevoie de control fin: Node + uWS custom sau Elixir Phoenix daca echipa cunoaste BEAM.
  • Joc competitiv cu matchmaking/social/leaderboards solide, plan de crestere 10–50k CCU: Nakama (Go) + Postgres/Redis.
  • Fizica grea/60 Hz/optimizari hardcore: Rust pentru game servers, cu gateway JS/Elixir pentru orchestrare.
  • P2P party mic, cost scazut de server: WebRTC, dar cu server de autoritate minimal sau verificari stricte, si TURN bugetat.

Operare: cum le pui pe picioare la scara

  • Orchestrare: Docker + Nomad sau Kubernetes simplu (un chart per componenta). Evita sidecar hell.
  • Health checks: soft-kill camerele dupa match, reincarca procesele care depasesc 70% mem stabil timp de X minute.
  • Deploy canarizat: 5% camere pe noua versiune, restul pe vechi; evita update mid-match.
  • Backups si migrare schemi: gh-ost/pgmig/Prisma Migrate; freeze in ferestre bine definite.
  • Feature flags: LaunchDarkly sau homegrown in Redis pentru a activa netcode nou pe subset de camere.

Exemplu de pipeline de build si rulare

  • Monorepo: client (TS) + server (Node/Go) + shared proto definitions.
  • CI: build + test + e2e cu simulatoare headless care trimit input random si lovesc timpii-limita.
  • CD: blue/green pe game servers; baza de date cu migrare backward-compatible.

FAQ

WebRTC nu e mai rapid decat WebSocket pentru jocuri?

Teoretic, QUIC/SCTP pot evita HOL si ofera partial reliability; practic, costul de operare (ICE, STUN/TURN, NAT edge cases) si anti-cheat fac WebRTC mai potrivit pentru P2P sau roomuri mici. Pentru server autoritativ, WebSocket ramane mai simplu si suficient in 90% din cazuri.

Pot folosi Socket.IO pentru un shooter 60 Hz?

Da, dar nu ar trebui. Socket.IO adauga overhead (ack-uri, reconectare, namespacing) si ruleaza peste WebSocket/long-polling. Pentru tick ridicat si pachete mici, foloseste uWebSockets.js sau ws raw, ori Colyseus care este mai eficient.

Redis poate stoca starea camerei?

Nu la tick. Redis e excelent pentru coordonare, cache, rate limits si leaderboards. Starea de joc se tine in memorie in procesul care simuleaza; orice cross-process sincron la 20–60 Hz va aduce lag si cost.

Cum reduc costul de trafic?

  • Delta snapshots in loc de full state.
  • Interest management agresiv.
  • Compresie binara (deflate per mesaj nu merita; mai bine mesaje mici de la inceput).
  • Tick 20–30 Hz in loc de 60 Hz, cu client-side prediction buna.

Ce baza de date: Postgres vs Cockroach vs Dynamo?

Postgres acopera 80% din nevoi la cost mic si predictibil. Cockroach daca ai nevoie de writes multi-region cu consistenta; Dynamo/NoSQL daca modelul tau e cheie-valoare masiv si vrei latente constante, dar cu tranzactii limitate.

Pot rula totul pe Cloudflare Workers/Durable Objects?

Pentru jocuri casual cu camere mici, DO pot fi foarte potrivite (partitii naturale per room, stateful la edge). Pentru tick mare si volum de trafic binar constant, limita de CPU si conexiuni devine bariera. Bun pentru prototipuri si jocuri turn-based/co-op usor.

Concluzii cheie

  • WebSocket autoritativ ramane baza pragmatica pentru jocurile multiplayer realtime in browser; WebRTC este nisa pentru P2P si canale speciale.
  • Alege stackul dupa CCU tinta si gen: Node/Colyseus pentru lansare rapida, Nakama pentru feature-set si scalare, Elixir/Rust pentru latency si headroom.
  • Camerele sunt unitatea ta de scalare; pastreaza-le independente si limiteaza dimensiunea lor.
  • Interest management si protocol binar scad costurile si lagul mai mult decat orice optimizare micro.
  • Observabilitatea (p95/99 RTT, drops, backlog per room) face diferenta intre pompieri si ingineri.

Daca construiesti un joc HTML5/WebGL si vrei sa discuti stiva front-end pentru randare si input, vezi si articolul nostru despre Three.js vs Babylon.js vs PixiJS.

La final: daca esti in dubiu, incepe simplu, masoara, si schimba cand ai dovezi — nu invers.

FAQ

Ce tick rate recomandati pentru un joc arcade 2D?

20–30 Hz pe server cu client-side prediction si reconciliere. Mai sus de atat aduce costuri si castiguri marginale, cu exceptii pentru fizica precisa.

Cati jucatori per camera e sanatos?

10–20 pentru actiune rapida; 30–50 pentru co-op lent. Dupa 20, interest management devine obligatoriu.

Cum fac matchmaking echitabil si rapid?

Combina scor MMR/Elo cu ping si timp de asteptare. Un algorim simplu: extinde fereastra de MMR la fiecare 5 secunde, prioritizand ping mai mic si parteneri recenti evitati.

Pot folosi GraphQL pentru realtime?

Pentru lobby/social, da (GraphQL Subscriptions). Pentru tick de joc, nu; overheadul e semnificativ vs. WebSocket binar custom.

Ce monitorizez in productie?

p50/p95/p99 RTT per room, drops la input/snapshot, GC time, CPU per room, mem per room, reconnect rate, erori de autentificare, si timpi de commit DB pentru operatiuni critice.

Ce fac cu cheaterii?

Server autoritativ, ratelimiting input, verificari fizice, logica de hit reg pe server, shadow bans cand e clar, si un pipeline de review. Nu promite perfectiune; promite frictiune pentru cheater.

Key takeaways

  • WebSocket autoritativ + interest management este solutia pragmatica pentru majoritatea jocurilor browser.
  • Colyseus si Nakama scurteaza timpul pana la un MVP solid; Rust/Elixir ofera headroom cand latenta/CPU devin limite.
  • Camerele independente, sticky LB si Redis doar pentru coordonare mentin arhitectura simpla si scalabila.
  • Protocol binar mic si tick 20–30 Hz reduc costuri si lag semnificativ.
  • Observabilitatea si limitarile stricte (rate limits, marime camera) previn caderi in productie.

Daca construiesti un joc multiplayer in browser si ai nevoie de un backend care sa tina pasul cu jucatorii reali, scrie-ne. Incepem cu o sesiune tehnica si un plan de livrare. Contact: /contact

URMATORUL PAS

Ti-a placut abordarea?

Aplicam aceleasi principii in proiectele clientilor: AI, automatizari, produse care nu se sting dupa lansare.