Multiplayer Netcode skill for Claude Code

Instant input, honest servers, no rubber-banding. Claude picks the right networking model for the genre, then builds it properly: inputs with sequence numbers, a predicted local player that reconciles by replaying unacknowledged inputs, interpolated remote entities, and capped server-side lag compensation. It validates every client command, trims bandwidth with quantization and interest management, and tests under simulated latency and loss.

  • namemultiplayer-netcode
  • size156 lines · ≈2,300 tokens
  • files2
  • licenseMIT
  • updated

What Claude sees before loading it

This description is the only part of the skill that sits in context all the time. Claude reads it to decide when to load the full playbook below.

description: Designs and debugs real-time multiplayer netcode with authoritative servers, client-side prediction and server reconciliation, entity interpolation, lag compensation, rollback for fighting games, deterministic lockstep, snapshot and delta compression, interest management, and cheat-resistant input validation. Use when building online multiplayer in any engine or custom stack, when players rubber-band, remote characters jitter, hits do not register, clients desync, bandwidth is too high, or when choosing between client-server, lockstep, and rollback.

Try asking

  • Players rubber-band whenever ping goes above 100 ms, find out why our prediction fails
  • Add client-side prediction and reconciliation to this Node.js game server and browser client
  • Should our 1v1 fighting game use rollback or delay-based netcode? Sketch the architecture

Use it when

  • Building real-time online multiplayer in any engine or custom stack
  • Rubber-banding, jittery remote players or missed hits
  • Lockstep or rollback clients desync
  • Per-client bandwidth is too high

Skip it when

  • Chat, presence or dashboards over WebSockets with no simulation — use realtime-websockets
  • Local physics bugs that also happen offline — use game-physics

How to check the result

Two clients under 150 ms RTT, jitter and 3% loss move smoothly with near-zero prediction error, hits register from the shooter's view, and bandwidth stays within budget.

The common mistake it prevents

Running client and server movement with different timesteps or code paths, so every reconciliation finds an error and the player rubber-bands.

~/.claude/skills/multiplayer-netcode/SKILL.md

--- name: multiplayer-netcode description: Designs and debugs real-time multiplayer netcode with authoritative servers, client-side prediction and server reconciliation, entity… license: MIT ---

Done well means: the server owns the truth, clients send inputs rather than state, the local player responds instantly through prediction and reconciles without visible snaps, remote entities interpolate smoothly, hits are judged from the shooter’s point of view within a capped window, and all of it has been played under simulated 150 ms latency, jitter, and packet loss.

Ground rules

  • Never trust the client. Clients send intent (inputs, commands with sequence numbers); the server validates and simulates. Client-authoritative positions are a cheat vector.
  • Simulation runs on a fixed tick on both sides; prediction is impossible to reconcile if client and server step with different dt.
  • Design for the network from the first prototype. Retrofitting prediction onto a game whose movement code reads the frame delta and mutates state everywhere is a rewrite.
  • Test under bad conditions every time: 100-200 ms RTT, 20-40 ms jitter, 1-5% loss. Localhost hides every bug in this skill.
  • Pick the engine’s netcode stack before writing your own; custom netcode is justified for custom engines, browser games, or unusual models (rollback, huge entity counts).

Choose the model

Game typeModelWhy
Shooter, action, MMO, co-opAuthoritative server + client prediction + interpolation + lag compensationCheat resistance, tolerates many players
Fighting game, 2-4 player precise actionRollback (GGPO-style) over P2P or relayInput-delay-free feel, tiny state
RTS with thousands of unitsDeterministic lockstep (inputs only)Bandwidth independent of unit count
Turn-based, card, boardReliable messages over WebSocket/HTTP; server validates movesLatency tolerant, simplicity
Casual co-op with friendsListen server/host + relayCheap; accept host advantage and host migration complexity

Workflow

- [ ] 1. Pick the model (table above) and the stack (engine netcode, library, or custom)
- [ ] 2. Make the simulation a pure function: state + inputs + fixed dt -> next state
- [ ] 3. Define messages: input commands (seq, tick, buttons), snapshots (tick, entities), acks
- [ ] 4. Implement prediction + reconciliation for the local player
- [ ] 5. Implement interpolation for remote entities
- [ ] 6. Add lag compensation for hit detection (if hitscan or fast projectiles)
- [ ] 7. Add server validation and rate limits on every client message
- [ ] 8. Measure bandwidth per client; quantize, delta-compress, cull by relevance
- [ ] 9. Play-test under network simulation; fix snaps and jitter

Core techniques

TechniqueWhat it solvesKey parameters
Client predictionInput latency for the local playerBuffer of unacked inputs with sequence numbers
Server reconciliationPrediction errorsOn snapshot: reset to server state for acked seq, replay pending inputs
Error smoothingVisible snaps after reconciliationBlend visual position toward corrected position over 100-200 ms; snap if error is large
Entity interpolationRemote entities jitter between snapshotsRender 2-3 snapshot intervals in the past (about 100 ms at 20-30 Hz)
Extrapolation (dead reckoning)Late snapshotsCap at 100-250 ms, then freeze; never for long
Lag compensation“I hit him on my screen”Server keeps hitbox history about 1 s; rewind to shooter’s view time, cap rewind at 200-250 ms
Input redundancyLost input packetsSend the last 3-5 inputs in each packet (unreliable channel)
RollbackFighting-game responsivenessSave state per frame; 1-3 frames input delay; roll back up to about 7-8 frames

For complete client, server, interpolation-buffer, lag-compensation, and rollback loops in code, read references/prediction-reconciliation.md when implementing or debugging any of these by hand.

Tick rates and channels

  • Server simulation 30-60 Hz for action games (fighting games run at render rate, 60 Hz); snapshot send rate 10-30 Hz; client input send rate equal to the client tick.
  • Unreliable sequenced channel for inputs and snapshots (newest wins); reliable ordered channel for chat, inventory changes, match events. Never send per-tick state over a reliable stream; head-of-line blocking turns 2% loss into seconds of stall.
  • Transports: UDP-based (ENet, Steam Networking Sockets, Unity Transport, engine defaults); browsers use WebRTC data channels or WebTransport for unreliable delivery, WebSocket only where TCP latency is acceptable.

Bandwidth

  • Quantize: positions to centimeters in a bounded range (e.g. 18-20 bits per axis), rotations as smallest-three quaternions or a yaw byte, velocities to fixed point.
  • Delta-compress snapshots against the last snapshot the client acknowledged; send full state when no ack is recent.
  • Interest management: send only entities relevant to each client (distance, line of sight, team). It also prevents wallhack data leaks.
  • Budget example: 64 players x 30 Hz x about 40 bytes per entity in view must fit the per-client downstream target (for many games, under about 100-200 kbps).

Security and validation

  • Validate every command server-side: movement speed and acceleration limits, cooldowns, ammo, line of sight, ownership of the target entity.
  • Rate-limit messages per connection; drop and log clients that exceed it.
  • Never send hidden information (enemy positions behind walls, opponent’s hand) to the client; filter at the server.
  • Hit validation: client may claim a hit with the tick it saw; the server rewinds and re-tests, rejecting claims outside the compensation window.

Engine stacks

EngineStarting point
UnityNetcode for GameObjects (simple co-op), Netcode for Entities (prediction built in), or third-party (Fish-Net, Mirror, Photon Fusion)
UnrealBuilt-in replication; Character Movement Component prediction; Gameplay Ability System for predicted abilities
Godot 4ENetMultiplayerPeer, MultiplayerSpawner, MultiplayerSynchronizer, RPCs; prediction is your job
Web / customColyseus or custom Node/Go/Rust server; Geckos.io or WebTransport for unreliable messaging

Network simulation for testing

# Linux: add 100 ms +/- 20 ms delay and 2% loss on loopback (undo with 'del')
sudo tc qdisc add dev lo root netem delay 100ms 20ms loss 2%
sudo tc qdisc del dev lo root
  • Windows: clumsy. macOS: Network Link Conditioner (Xcode additional tools).
  • Unreal console: Net PktLag=150, Net PktLoss=3. Unity Transport and Netcode tools include a network simulator component.
  • Confirm with the user before running tc commands on a shared or production machine.

Symptom table

SymptomLikely causeFix
Local player rubber-bands constantlyClient and server simulations differ (variable dt, different collision, float order)Share the exact movement code and fixed tick; log predicted vs server state per seq
Rubber-banding only under lossInputs lost, server simulates “no input”Input redundancy; server repeats last input for a few ticks
Remote players jitter or teleportRendering latest snapshot directlyInterpolation buffer with jitter allowance
Hits miss that looked cleanNo lag compensation, or wrong rewind timeRewind to (server tick - client interpolation delay - RTT/2), cap the window
Getting shot behind coverLag compensation favors shooterExpected trade-off; lower the cap or tick-align cover checks
Lockstep desync after minutesNondeterminism: float differences, unordered iteration, unseeded RNGPer-tick state hash exchange, find first diverging tick
Actions fire twiceReliable message replayed or input applied in both predict and replay pathsIdempotent commands keyed by sequence number

Gotchas

  • Clock sync: estimate RTT and server tick offset continuously (moving average, discard outliers); the client should run slightly ahead of the server so inputs arrive in time.
  • Spawning predicted entities (projectiles) on the client needs IDs that the server can match; otherwise you get duplicates or disappearing bullets.
  • Physics engines are rarely deterministic enough for rollback or lockstep across machines; use a deterministic engine/config or simple custom movement.
  • NAT traversal fails for a meaningful share of players; P2P designs need a relay.
  • Host migration is hard; prefer dedicated or relay-hosted authority if sessions are long.

Verification

  1. Two or more clients under 150 ms RTT, 30 ms jitter, 3% loss: local movement feels immediate, corrections are not visible in normal play, remote players move smoothly.
  2. Prediction error log (predicted vs authoritative per input seq) is near zero in steady movement.
  3. Hit registration test: shots on a strafing target at 150 ms RTT register when the crosshair was on target in the shooter’s view.
  4. Bandwidth per client measured at peak player count stays inside the budget.
  5. Fuzz the server with malformed and out-of-range commands; nothing crashes, cheats are rejected and logged.

Bundled files

Loaded only when a step in the skill needs them. Review them here before installing.

references/prediction-reconciliation.md 191 lines · 7.5 KB
# Prediction, reconciliation, interpolation, lag compensation, rollback

Reference loops in TypeScript-style pseudocode. They assume one shared, deterministic
`simulate(state, input, dt)` used by both client and server; that shared function is the
single most important property of working netcode.

```ts
const TICK = 1 / 60;
type Input = { seq: number; tick: number; moveX: number; moveY: number; buttons: number };
type PlayerState = { x: number; y: number; vx: number; vy: number };

function simulate(s: PlayerState, i: Input, dt: number): PlayerState {
  const speed = 6;
  const vx = i.moveX * speed, vy = i.moveY * speed;
  return { x: s.x + vx * dt, y: s.y + vy * dt, vx, vy };
}
```

## 1. Client: prediction and reconciliation

```ts
class PredictedPlayer {
  state: PlayerState = { x: 0, y: 0, vx: 0, vy: 0 };
  pending: Input[] = [];          // sent but not yet acknowledged by the server
  seq = 0;
  visualOffset = { x: 0, y: 0 };  // for smoothing corrections

  tick(raw: { moveX: number; moveY: number; buttons: number }, clientTick: number) {
    const input: Input = { seq: ++this.seq, tick: clientTick, ...raw };
    this.pending.push(input);
    send({ type: 'input', inputs: this.pending.slice(-4) });   // redundancy: last 4 inputs
    this.state = simulate(this.state, input, TICK);            // predict immediately
  }

  onServerSnapshot(authoritative: PlayerState, lastProcessedSeq: number) {
    const before = { x: this.state.x, y: this.state.y };
    // 1. drop inputs the server has processed
    this.pending = this.pending.filter((i) => i.seq > lastProcessedSeq);
    // 2. rewind to server truth, 3. replay the rest
    let s = authoritative;
    for (const i of this.pending) s = simulate(s, i, TICK);
    this.state = s;
    // 4. smooth small corrections, snap large ones
    const ex = before.x - s.x, ey = before.y - s.y;
    if (Math.hypot(ex, ey) < 2) { this.visualOffset.x += ex; this.visualOffset.y += ey; }
    else this.visualOffset = { x: 0, y: 0 };
  }

  renderPosition(dtFrame: number) {
    const decay = Math.exp(-dtFrame / 0.1);                    // ~100 ms smoothing
    this.visualOffset.x *= decay; this.visualOffset.y *= decay;
    return { x: this.state.x + this.visualOffset.x, y: this.state.y + this.visualOffset.y };
  }
}
```

Debug aid: log `(seq, predicted, authoritative)` pairs. Non-zero error in straight-line
movement means the two simulations differ; find the divergent input, not a bigger smoothing
factor.

## 2. Server: authoritative tick

```ts
class ServerPlayer {
  state: PlayerState = { x: 0, y: 0, vx: 0, vy: 0 };
  queue = new Map<number, Input>();   // seq -> input
  lastProcessedSeq = 0;
  lastInput: Input | null = null;

  receive(inputs: Input[]) {
    for (const i of inputs) {
      if (i.seq <= this.lastProcessedSeq || this.queue.has(i.seq)) continue;  // duplicate
      if (!isValid(i)) { flag(i); continue; }   // clamp axes to [-1,1], known buttons only
      this.queue.set(i.seq, i);
    }
  }

  tick() {
    const next = this.queue.get(this.lastProcessedSeq + 1);
    const input = next ?? (this.lastInput && { ...this.lastInput, seq: this.lastProcessedSeq });
    if (next) { this.queue.delete(next.seq); this.lastProcessedSeq = next.seq; this.lastInput = next; }
    if (input) this.state = simulate(this.state, input, TICK);
  }
}
// every snapshot interval: send each client its own state + lastProcessedSeq,
// and other entities' states stamped with the server tick
```

Process one input per tick per player. If a client floods inputs (speed hack by sending
more than one per tick), the queue grows; cap it (e.g. 8) and drop the excess.

## 3. Remote entity interpolation

```ts
type Snap = { serverTime: number; x: number; y: number };

class InterpolatedEntity {
  buffer: Snap[] = [];
  push(s: Snap) { this.buffer.push(s); if (this.buffer.length > 32) this.buffer.shift(); }

  sample(renderServerTime: number): { x: number; y: number } | null {
    // renderServerTime = estimatedServerNow - interpDelay (e.g. 100 ms)
    const b = this.buffer;
    for (let i = b.length - 1; i > 0; i--) {
      const a = b[i - 1], c = b[i];
      if (a.serverTime <= renderServerTime && renderServerTime <= c.serverTime) {
        const t = (renderServerTime - a.serverTime) / (c.serverTime - a.serverTime);
        return { x: a.x + (c.x - a.x) * t, y: a.y + (c.y - a.y) * t };
      }
    }
    const last = b[b.length - 1];
    return last ?? null;   // starved: hold last (or extrapolate briefly, capped)
  }
}
```

Choose `interpDelay` as about 2 snapshot intervals plus observed jitter. Track how often
the buffer starves; adapt the delay upward when it does.

## 4. Server-side lag compensation

```ts
type Hitbox = { id: number; x: number; y: number; r: number };
const history: { time: number; boxes: Hitbox[] }[] = [];   // ring buffer, ~1 s
const MAX_REWIND = 0.25;                                   // seconds

function recordHistory(now: number, boxes: Hitbox[]) {
  history.push({ time: now, boxes: boxes.map((b) => ({ ...b })) });
  while (history.length && now - history[0].time > 1) history.shift();
}

function validateShot(now: number, shooterViewTime: number, ray: Ray): number | null {
  // shooterViewTime = time the client was rendering remote entities at when it fired
  const t = Math.max(shooterViewTime, now - MAX_REWIND);
  const frame = closestFrames(history, t);            // interpolate between two records
  return raycastHitboxes(ray, frame);                  // id of the hit entity or null
}
```

The client sends with each fire command its current interpolation render time (or the tick
pair and fraction it was rendering). Never trust a client-supplied time beyond the cap.

## 5. Rollback loop (GGPO-style)

```ts
const INPUT_DELAY = 2;        // frames
const MAX_ROLLBACK = 8;       // frames
let frame = 0;
const states = new Map<number, GameState>();       // frame -> snapshot (save every frame)
const localInputs = new Map<number, PadInput>();
const remoteInputs = new Map<number, PadInput>();   // confirmed
let lastConfirmedRemote = -1;

function advance() {
  localInputs.set(frame + INPUT_DELAY, readPad());
  send({ frame: frame + INPUT_DELAY, input: localInputs.get(frame + INPUT_DELAY) });

  if (frame - lastConfirmedRemote > MAX_ROLLBACK) return;   // stall: too far ahead

  const remote = remoteInputs.get(frame) ?? predictRemote(frame);  // usually repeat last
  states.set(frame, save(current));
  current = step(current, localInputs.get(frame)!, remote);
  frame++;
}

function onRemoteInput(f: number, input: PadInput) {
  remoteInputs.set(f, input);
  lastConfirmedRemote = Math.max(lastConfirmedRemote, f);
  if (f < frame && input !== predictedFor(f)) {
    current = load(states.get(f)!);                 // roll back
    for (let g = f; g < frame; g++) {
      states.set(g, save(current));
      current = step(current, localInputs.get(g)!, remoteInputs.get(g) ?? predictRemote(g));
    }
  }
}
```

Requirements: `step` is deterministic and fast enough to run MAX_ROLLBACK + 1 times per
frame; `save`/`load` are cheap (flat arrays, no allocation); audio and particles are
triggered idempotently so a resimulated frame does not replay sounds. Exchange a state
checksum every N frames to detect desync early.

## 6. Clock and tick sync

- Ping/pong with timestamps every 1-2 s; RTT = now - sentTime; keep a median of the last
  10 samples.
- Server time estimate = serverTimestamp + RTT / 2.
- Client tick target = estimated server tick + (RTT / 2) / TICK + 1-2 ticks of safety, so
  inputs arrive just before the server needs them. Nudge by speeding or slowing the client
  tick by 1-5% rather than jumping.

About this skill

Written and maintained by the Claude Skills editors to the same editorial standard as every skill in the directory: a selective trigger, a real workflow, known pitfalls and a verification step. First published , last updated . MIT licensed.

esc