Why a game backend is not built like a web backend
A web backend answers a request and forgets who made it: the client asks, the server reads from the database, replies, and if the reply arrives half a second late nobody notices. A multiplayer game backend does a different job. It keeps a shared simulation in memory, advances it in regular steps (at 64 steps per second each one lasts 15.6 ms) and at every step tells each player what happened. If that process stops, the match stops with it.
Almost every choice in this series comes from that difference: servers that are stateful and therefore cannot be replicated behind a load balancer like an API, protocols that accept losing data, messages compressed by hand, sessions that survive a network drop. This first article builds the map: each component, the problem it solves and where mistakes happen. The following articles dig into one at a time, starting with dedicated game servers.
Web and game compared: where the rules change
The table lines up the differences that affect the design. It contains no universal latency thresholds, because there are none: they depend on the genre of the game and on how much your players tolerate, and whoever writes them as hard numbers is copying from someone else.
| Aspect | Web backend | Multiplayer game backend |
|---|---|---|
| Communication | Request and response | Continuous flow in both directions |
| Update pace | When the user acts | At a fixed frequency, tens of times per second (tick rate) |
| State | In the database; the server can be replaced at will | In memory on the match server |
| Data loss | Never tolerated on transactions | Tolerated for positions, never for purchases |
| Scaling | Add replicas behind a balancer | Add matches, each with its own process |
The row that weighs most is "State". A web server is thrown away and recreated: the next one finds everything in the database. A game server holds the positions, the projectiles in flight and the scores of a match in progress, and if the process dies no other instance knows where you were. That is why game servers are treated like cattle with a short life and a single mission, not as interchangeable replicas, and why real persistence (profiles, purchases) lives outside, in a database, and is written only at the right moments.
A numeric example shows why delay is not a detail. With an RTT (round trip) of 50 ms, assuming a symmetric path, your key press takes about 25 ms to reach the server and the updated state another 25 to come back: between the press and what you see at least 50 ms go by, plus the wait for the next tick. Gabriel Gambetta explains in his series on fast-paced multiplayer how this gap forces you to hide the network with client-side prediction and lag compensation: the subject of the article on state synchronization.
Network models: who decides what happened
The first choice is who has the final word on the state of the game. There are three answers and each moves the cost to a different place: onto the servers, onto security or onto the complexity of the clients.
Client-server with an authoritative server
The server is the only authority. Clients send intentions ("I want to move right"), not results ("I am at x=20"): the server validates them, updates the state and sends back the outcome. Gambetta's example is very clear: if the client could say "I am at (20,10)" right after "I am at (10,10)", a cheater would walk through walls; if it says "I move one square", the server computes (11,10) and sends it back. The price is the cost of one server process per match and the latency of a full round trip for every action.
Peer-to-peer
Every player talks to all the others and nobody has a server. With N players the connections are N·(N−1)/2: 28 for eight players, 4,950 for a hundred. Glenn Fiedler notes that this model survives in RTS games, where the commands of each turn are executed identically on all machines (lockstep), and he lists its limits: it needs perfect determinism, and every player gets the latency of the slowest one. Nobody checks anybody, so a tampered client has no judge.
Relay
A server forwards messages without understanding the game: it solves NAT (players do not have to reach each other directly) and costs little, but the simulation stays on the clients and so does the trust. It is an honest compromise for cooperative or casual games, where cheating costs the people playing with you very little.
| Model | Who decides | Server cost | Cheating | When it makes sense |
|---|---|---|---|---|
| Client-server | The server | One process per match | Hard | Competitive, many players |
| Peer-to-peer | Each client | Almost nil | Easy | Few players, among friends |
| Relay | The clients, with a postman | Low | Possible | Cooperative, casual |
The server-side game loop: the tick and its budget
The heart of the authoritative server is a fixed-frequency loop: collect the inputs, advance the simulation, send the state. Each iteration is a tick. The tick rate you choose fixes the budget: at 64 Hz you have 1000/64 = 15.625 ms to do everything, network included. If an iteration costs more, the next one starts late and every player perceives it as a stutter.
The listing below opens no sockets and no timers: it simulates 6,400 ticks (100 seconds) with a fake clock and costs
generated from a fixed seed, so the output is identical on every run. Each tick normally costs 4-8 ms, and in
one out of fifty a burst of 20-30 ms (numbers invented for the model, not measurements of a real game).
Two details matter. debt accumulates the delay: a 25 ms tick eats the budget of the following ones,
and it is the mechanism that turns an isolated spike into a visible stutter. And maxCatchUp is the threshold
beyond which the server stops chasing and drops ticks: in the output below it never triggers (the delay is reabsorbed by the light ticks), but without a threshold a server that is behind tries to catch up
by spinning idle as fast as it can and makes everything worse (the "death spiral").
// Fixed-tick server: time is simulated, so the result is reproducible.
const TICK_RATE = 64;
const TICK_MS = 1000 / TICK_RATE; // 15.625 ms of budget per tick
function mulberry32(seed: number): () => number {
let a = seed;
return () => {
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
// Simulated cost of a tick: normally 4-8 ms, in 1 tick out of 50 a burst of 20-30 ms.
function tickCost(rand: () => number): number {
return rand() < 0.02 ? 20 + rand() * 10 : 4 + rand() * 4;
}
function percentile(sorted: readonly number[], p: number): number {
// nearest-rank: the value under which at least p% of the samples fall
const rank = Math.ceil((p / 100) * sorted.length);
return sorted[Math.min(sorted.length, Math.max(1, rank)) - 1];
}
function simulate(totalTicks: number, maxCatchUp: number) {
const rand = mulberry32(42);
const costs: number[] = [];
let overruns = 0;
let dropped = 0;
let debt = 0; // milliseconds of delay accumulated against the ideal clock
for (let tick = 1; tick <= totalTicks; tick++) {
const cost = tickCost(rand);
costs.push(cost);
if (cost > TICK_MS) overruns++;
debt = Math.max(0, debt + cost - TICK_MS); // the long tick eats the budget of the following ones
while (debt >= TICK_MS * maxCatchUp) { // past the threshold ticks are dropped, not caught up
debt -= TICK_MS;
dropped++;
}
}
const sorted = [...costs].sort((x, y) => x - y);
const mean = costs.reduce((s, c) => s + c, 0) / costs.length;
return { mean, p50: percentile(sorted, 50), p99: percentile(sorted, 99), max: sorted[sorted.length - 1], overruns, dropped };
}
const r = simulate(6400, 3); // 100 seconds at 64 ticks/s
console.log(`budget per tick : ${TICK_MS.toFixed(3)} ms`);
console.log(`mean duration : ${r.mean.toFixed(2)} ms (${((r.mean / TICK_MS) * 100).toFixed(0)}% of the budget)`);
console.log(`p50 / p99 / max : ${r.p50.toFixed(2)} / ${r.p99.toFixed(2)} / ${r.max.toFixed(2)} ms`);
console.log(`ticks over budget: ${r.overruns} out of 6400, dropped ticks: ${r.dropped}`);
// Output:
// budget per tick : 15.625 ms
// mean duration : 6.35 ms (41% of the budget)
// p50 / p99 / max : 6.04 / 25.18 / 29.83 ms
// ticks over budget: 114 out of 6400, dropped ticks: 0
The output holds the most useful lesson of the article. The mean duration is 41% of the budget:
looking at the mean alone the server seems rested. But the p99 is 25 ms, almost one and a half times the budget, and 114
ticks out of 6,400 (1.8%) exceed it: on average one every 0.9 seconds, so somebody sees a stutter. That is why in production
you watch a high percentile, not the mean. The percentile function uses the nearest-rank method:
it sorts the samples and takes the one at position ⌈p·n⌉. Do not use this code as it is in production: a real
setInterval drifts from the ideal time, and has to be corrected with the time actually elapsed.
The three levels of state
Game state is not a single block. It is worth splitting it by how often it changes and by how much it hurts to lose it, because from this follows where it lives in memory, what you send over the network and what you write to disk.
| Level | Examples | When it changes | Where it lives |
|---|---|---|---|
| Frame state | Positions, velocities, projectiles | Every tick | Only in the RAM of the game server |
| Session state | Health, inventory, score | On an event (damage, pickup) | RAM, with a copy in a cache such as Redis if the match has to survive a crash |
| Persistent state | Profile, purchases, leaderboard | At the end of the match or in a transaction | Relational database |
Read the table from the bottom up as a ladder of pain: losing a position costs an instant, losing a purchase costs a refund and a customer. The frame state must be kept compact because you send it at every tick, the session state is sent only when it changes, the persistent state is never broadcast and is written asynchronously. Writing the profile to PostgreSQL at every single hit would be the quickest way to break the tick budget.
Protocols: how much guaranteed order is worth
TCP delivers everything, in the right order. If a packet is lost, the following ones wait for it to be retransmitted: this is head-of-line blocking. For a chat it is fine; for a player's position it is harmful, because while you wait for the old packet, newer ones have arrived that you cannot use. UDP gives up the guarantees and lets you discard the outdated data. The price is that reliability, where you need it, you write yourself.
In the browser UDP does not exist directly. WebSocket (RFC 6455) is a bidirectional channel over TCP: simple and universal, with head-of-line blocking. WebRTC data channels (RFC 8831) use SCTP over DTLS over UDP, and for each channel you can choose whether to be reliable or not: the same RFC cites as a use case a real-time game with positions and state sent over unreliable channels. WebTransport rests on HTTP/3: it is currently a W3C Candidate Recommendation (snapshot of 30 July 2026), and server-side support is still scarcer than for WebSocket, so evaluate it case by case.
| Protocol | Transport | Order and delivery | Head-of-line blocking | In the browser |
|---|---|---|---|---|
| TCP | TCP | Guaranteed | Yes | No (direct) |
| UDP | UDP | No guarantee | No | No |
| WebSocket | TCP | Guaranteed | Yes | Yes |
| WebRTC data channel | SCTP over DTLS over UDP | Configurable per channel | No, if unreliable | Yes |
Serious games do not pick a single channel: they use two, one for data that ages and one for data that must not be lost. What goes through the unreliable channel (positions, rotations, effects) is replaced by the next tick, so retransmitting it is wasted time. What goes through the reliable channel (damage, weapon switch, chat, matchmaking commands, transactions) has no next tick to correct it: if it is lost, the state stays wrong. The rule for deciding is to ask what happens if this message does not arrive and nobody repeats it.
Serialization: counting bytes
Multiply: if every client receives the state of 100 players at every tick, each byte of a single state weighs 100
times 64 per second, that is 6,400, and for every connected client. The listing measures the same state of one player in
four formats. For Protocol Buffers and MessagePack I used the libraries
protobufjs and @msgpack/msgpack, installed in a temporary test folder; the
"hand-made binary" is a DataView with positions in centimeters on 32 bits and angles in tenths of a degree on 16.
import protobuf from "protobufjs";
import { encode as mpEncode } from "@msgpack/msgpack";
const state = {
type: "player_state", playerId: "p_abc123",
posX: 123.456, posY: 78.901, posZ: 45.678,
rotYaw: 180.5, rotPitch: -12.3, health: 85, weapon: "rifle", tick: 15042,
};
const root = protobuf.parse(`
syntax = "proto3";
message PlayerState {
uint32 player_id = 1;
float pos_x = 2; float pos_y = 3; float pos_z = 4;
float rot_yaw = 5; float rot_pitch = 6;
uint32 health = 7; uint32 weapon = 8; uint32 tick = 9;
}`).root;
const PlayerState = root.lookupType("PlayerState");
const pb = PlayerState.encode(PlayerState.create({
playerId: 123, posX: state.posX, posY: state.posY, posZ: state.posZ,
rotYaw: state.rotYaw, rotPitch: state.rotPitch, health: 85, weapon: 3, tick: 15042,
})).finish();
// Hand-made format: positions in centimeters as int32, angles in tenths of a degree as int16
const buf = new ArrayBuffer(20);
const v = new DataView(buf);
v.setUint16(0, 123);
v.setInt32(2, Math.round(state.posX * 100)); v.setInt32(6, Math.round(state.posY * 100));
v.setInt32(10, Math.round(state.posZ * 100)); v.setInt16(14, Math.round(state.rotYaw * 10));
v.setInt16(16, Math.round(state.rotPitch * 10)); v.setUint8(18, 85); v.setUint8(19, 3);
const sizes = {
"JSON (text)": Buffer.byteLength(JSON.stringify(state)),
"MessagePack": mpEncode(state).length,
"Protocol Buffers": pb.length,
"hand-made binary": buf.byteLength,
};
const base = sizes["JSON (text)"];
for (const [name, n] of Object.entries(sizes)) {
console.log(`${name.padEnd(17)} ${String(n).padStart(4)} bytes ${((n / base) * 100).toFixed(0)}% of JSON`);
}
// Every client receives the state of 100 players at every tick
for (const [name, n] of Object.entries(sizes)) {
console.log(`${name.padEnd(17)} ${((n * 100 * 64) / 1000).toFixed(1)} KB/s to each client`);
}
// Output:
// JSON (text) 162 bytes 100% of JSON
// MessagePack 142 bytes 88% of JSON
// Protocol Buffers 34 bytes 21% of JSON
// hand-made binary 20 bytes 12% of JSON
// JSON (text) 1036.8 KB/s to each client
// MessagePack 908.8 KB/s to each client
// Protocol Buffers 217.6 KB/s to each client
// hand-made binary 128.0 KB/s to each client
The numbers disprove two commonplaces. MessagePack, being "a binary JSON", carries the field names in every message and here saves only 12%: it gives a real advantage when the data is small or not very repetitive, not for state streams where the fields are always the same. Protocol Buffers (encoding guide) replaces the names with field numbers and encodes integers with a variable length, and gets to 21% of JSON; the hand-made format, which quantizes the coordinates, goes down to 12%. The gain, however, is paid for: the schema has to be kept in agreement with all the clients, and quantized values lose precision (one centimeter, here).
The second surprise is the last four lines: even with the most compact format, sending every client all 100 players at every tick costs 128 KB/s, and with JSON over a megabyte per second, per client. No game really does that: it filters what the player can see (interest management) and sends only the differences from the last confirmed state. Before looking for the smallest format, ask yourself how many states you send.
Sessions, heartbeats and reconnection
A mobile connection drops when it changes cell, the Wi-Fi dies, the player steps into an elevator. If every drop destroyed the session, the player would lose the match over one second of network. The solution is to separate the connection, which is fragile, from the session, which the server keeps alive for a window of time: if the player comes back inside the window, they receive the full state and resume.
The listing models three roles. The heartbeat updates lastHeartbeatMs and reports RTT and jitter;
sweep is the periodic check that suspends (after 15 s of silence) and then closes (after another 60 s) the
session; reconnect works only from the suspended state. Two choices to note. First, the RTT is measured
by the client (it sends a ping, receives the echo, subtracts its own clocks) and reported to the server: computing
Date.now() - clientTimestamp on the server, as is often seen, compares two unsynchronized
clocks and produces meaningless numbers. Second, sessions are immutable values: every transition
returns a new object, and this makes the edge cases testable one by one.
// Session with heartbeat and reconnection window. Simulated clock, no network.
type Status = "connected" | "disconnected" | "closed";
interface Session {
readonly playerId: string;
readonly status: Status;
readonly lastHeartbeatMs: number;
readonly disconnectedAtMs: number | null;
readonly rttMs: number;
readonly jitterMs: number;
}
const DISCONNECT_TIMEOUT_MS = 15_000;
const RECONNECT_WINDOW_MS = 60_000;
const open = (playerId: string, nowMs: number): Session =>
({ playerId, status: "connected", lastHeartbeatMs: nowMs, disconnectedAtMs: null, rttMs: 0, jitterMs: 0 });
// The client measures the RTT: it sends a ping, the server echoes it back, the client
// subtracts its own clock from its own clock. The server only receives the result.
const onHeartbeat = (s: Session, nowMs: number, rttReportedMs: number): Session =>
({ ...s, lastHeartbeatMs: nowMs, rttMs: rttReportedMs, jitterMs: Math.abs(rttReportedMs - s.rttMs) });
function sweep(s: Session, nowMs: number, log: (m: string) => void): Session {
if (s.status === "connected" && nowMs - s.lastHeartbeatMs > DISCONNECT_TIMEOUT_MS) {
log(`t=${nowMs / 1000}s ${s.playerId}: no heartbeat for 15 s, session suspended`);
return { ...s, status: "disconnected", disconnectedAtMs: nowMs };
}
if (s.status === "disconnected" && nowMs - (s.disconnectedAtMs ?? nowMs) > RECONNECT_WINDOW_MS) {
log(`t=${nowMs / 1000}s ${s.playerId}: 60 s window expired, session closed`);
return { ...s, status: "closed" };
}
return s;
}
function reconnect(s: Session, nowMs: number, log: (m: string) => void): Session {
if (s.status !== "disconnected") {
log(`t=${nowMs / 1000}s ${s.playerId}: reconnection refused (status ${s.status}), start again from login`);
return s;
}
log(`t=${nowMs / 1000}s ${s.playerId}: reconnected, the server sends the full state`);
return { ...s, status: "connected", lastHeartbeatMs: nowMs, disconnectedAtMs: null };
}
const log = (m: string) => console.log(m);
let ana = open("ana", 0);
let bruno = open("bruno", 0);
const rttAna = [40, 44, 90, 41]; // the third sample is a network spike
for (let i = 0; i < 4; i++) { ana = onHeartbeat(ana, (i + 1) * 2000, rttAna[i]); }
console.log(`ana: last RTT ${ana.rttMs} ms, jitter ${ana.jitterMs} ms`);
for (let t = 1000; t <= 100_000; t += 1000) {
ana = sweep(ana, t, log);
bruno = sweep(bruno, t, log);
if (t === 50_000) ana = reconnect(ana, t, log); // inside the window
if (t === 100_000) bruno = reconnect(bruno, t, log); // window already closed
}
// Output:
// ana: last RTT 41 ms, jitter 49 ms
// t=16s bruno: no heartbeat for 15 s, session suspended
// t=24s ana: no heartbeat for 15 s, session suspended
// t=50s ana: reconnected, the server sends the full state
// t=66s ana: no heartbeat for 15 s, session suspended
// t=77s bruno: 60 s window expired, session closed
// t=100s bruno: reconnection refused (status closed), start again from login
The output shows the two fates. bruno sends no heartbeat: at 16 s he is suspended, at 77 s the
window expires, and the attempt to rejoin at 100 s is refused, so the client has to log in again. ana
reconnects at 50 s, inside the window, and resumes from the full state. Then, having no more heartbeats in the
simulation, she is suspended again at 66 s: this is an artifact of the model, not a defect. The times (15 and 60 seconds) are product
choices, not standards: a punishing team game shortens the window, a cooperative one lengthens it.
The data stack: one database for each kind of data
No database serves all of a game's data well, which is why you compose a stack. The fastest level is the memory of the process, where the frame state lives. A distributed cache such as Redis holds the session state, the leaderboards (sorted sets lend themselves well to ordering by score) and the matchmaking queues. A relational database such as PostgreSQL keeps profiles, inventories and purchases because it gives transactions, which in a purchase are not negotiable. A time-series database collects telemetry, and object storage keeps replays and backups. Each level is slower and more durable than the previous one: the classic mistake is writing every event to the wrong level, that is, the slowest.
Infrastructure: from the process to the fleet
The dedicated server is a process that simulates a match on hardware of yours, unlike the
listen server, where a player also acts as host and the match dies with their connection. It is packaged
in a container, because starting an instance has to cost seconds, not minutes.
What remains is the problem of managing hundreds of matches: who starts them, who assigns them to players, who shuts them down. An
orchestrator solves it. Agones, for example, is a library
for hosting, running and scaling dedicated game servers on Kubernetes, and it uses custom resources
(GameServer, Fleet, Fleetautoscaler) to make the fleet scale with demand.
The managed alternatives and the comparison are in the article on
dedicated servers.
The diagram below lines up the pieces. It reads from the top: the player enters from the edge (balancer), talks to the "web-style" services (login, store, leaderboard, matchmaking) and is routed to a game server assigned by the orchestrator, which then works on its own during the match.
Client --> Edge (balancer, TLS) --> API Gateway
|
+-----------+-----------+------+-------+-------------+
Auth Matchmaker Store Leaderboard Social
|
Orchestrator (assigns a match to a game server)
|
+---------------+---------------+
Game server 1 Game server 2 Game server N (one match each)
|
Cache (session state) --> Database (persistence, at the end of the match)
The drawing is a schema, not a runnable listing: no specific technology is prescribed. It shows the structural point: the services on the left are stateless and scale like any API, the block on the right is stateful and scales per match. If you mix the two worlds in the same process, you lose the advantages of both.
Scaling patterns: match, world, instance
How you scale depends on the genre. In match-based games (shooters, battle royale) each match is an isolated instance with a fixed number of players: it ends, the process is destroyed, and scaling means having more instances. In persistent worlds the world is divided into zones, each handled by one server, and players move from one zone to another with a transfer of their state (handoff). A middle way is instanced zones: the world is persistent but a dungeon is copied for each group, so high density stays confined to instances that you can create and shut down.
When to buy a BaaS and when to build
There are platforms that offer matchmaking, leaderboards, authentication and storage ready-made. Among the open source ones, Nakama (a distributed server for social and real-time games) and Colyseus (a multiplayer framework for Node.js) can be self-hosted; the article on Open Match and Nakama covers them. The decision is economic before it is technical, and these three combinations work in the following cases.
- Ready-made platform: if you have few developers, an ordinary genre (cards, puzzle, casual) and the release date as the main constraint.
- Custom build: if the game logic or the netcode is your competitive edge, or if control over anti-cheat cannot be delegated.
- Hybrid: a platform for identity, leaderboards and store, your own game server for the simulation.
The criterion that ties the three items together is where your difference lies: build what sets you apart and buy what is the same for everyone. The hybrid is the most frequent choice because the simulation is specific to the game, while a login system or a leaderboard is not. The risk to weigh is dependency: every function you hand to a closed vendor is a function you will migrate with difficulty.
What to measure in production
An unobserved game backend is a time bomb, and the point is to choose metrics that tell you how the player feels, not how the machine is doing. The seven below are the ones to start from.
- CCU: concurrently connected players, the raw load.
- Tick duration, at p99: compared with the tick budget, as in the listing.
- RTT at p50, p95 and p99: the round-trip delay, by distribution and not by mean.
- Packet loss and jitter: the quality of the player's network, which you do not control.
- Outgoing bandwidth per player: the signal that interest management and deltas are working.
- Rate of successful reconnections: tells you whether the session window does its job.
- Server start time: how long a player waits when the fleet is full.
The first three decide whether to add capacity; the others tell you where the problem is. None of these thresholds has a universal value: pick yours from the data of your beta, and set the alarms on the tail (p99) and not on the mean, as the tick listing showed. We leave the tooling to the series dedicated to the observability of the game backend.
What you can do now
You can explain why a game server is not replicated like an API and what lives in memory as opposed to what goes into the database. You can compute the budget of a tick and recognize that the mean hides the stutters. You can decide which messages travel on an unreliable channel and which do not, and measure the weight of a format instead of trusting a table. You can design a session that survives a network drop. The next step is how matches are assigned to servers: the article on dedicated game servers; state synchronization, that is, how to hide the 50 ms, is in the fourth.
Frequently asked questions
Why does a game server not scale like a web API?
Because it holds the state of the match in memory: two instances are not interchangeable, and a balancer that moves a request elsewhere would send it to a process that knows nothing about the match. You scale by adding matches, not replicas of the same one.
UDP or WebSocket for a browser game?
Plain UDP does not exist in the browser. For a turn-based or slow-paced game WebSocket is enough; for a fast game WebRTC data channels allow unreliable delivery without head-of-line blocking. WebTransport is the future path, but server-side support is still limited.
Which tick rate should I choose?
The one your server can sustain at p99 within the budget, with the bandwidth you can afford: a higher tick rate reduces the simulation delay, but multiplies CPU and traffic. Start low, measure, then go up.
Sources
- Gabriel Gambetta, Fast-Paced Multiplayer, part I: Client-Server Game Architecture - the authoritative server principle and the case of the client that declares its own position, accessed on 2026-11-12
- Glenn Fiedler, What Every Programmer Needs To Know About Game Networking - peer-to-peer lockstep and its limits, accessed on 2026-11-12
- RFC 6455, The WebSocket Protocol - the bidirectional channel over TCP, accessed on 2026-11-12
- RFC 8831, WebRTC Data Channels - channels over SCTP/DTLS/UDP, reliable or not, and the real-time game use case, accessed on 2026-11-12
- W3C, WebTransport - Candidate Recommendation Snapshot status of 30 July 2026, accessed on 2026-11-12
- Protocol Buffers, Encoding - field numbers and variable-length integers, accessed on 2026-11-12
- Agones Documentation - the definition ("hosting, running and scaling dedicated game servers on Kubernetes") and the GameServer, Fleet, Fleetautoscaler resources, accessed on 2026-11-12
The listings were run with Node.js 24 (TypeScript with type stripping, protobufjs and @msgpack/msgpack installed in a temporary folder); the outputs shown are the real ones. Tick costs and sessions are a teaching model with a simulated clock and a fixed seed, not measurements of a game in production. The component schema is not runnable.
Commenti
Caricamento commenti...
Sign in to comment