Skip to content

Latency, jitter, and packet loss

In game performance testing, “latency” is narrower and stricter than the p95 response time you track for a web API. Players feel RTT in the tens of milliseconds. Jitter, the variance in that RTT, disrupts smooth gameplay even when the average latency is acceptable. Packet loss causes visible stuttering or desync at any rate above 0.5 %. This page explains which metrics show game health under load and how to capture and read them in MaxoPerf.

  • Read Game server load testing. The game server load test is where you capture these metrics.
  • Understand your game’s tick rate: the frequency (in Hz) at which the server processes world state and broadcasts updates to clients. Tick rate determines the minimum meaningful latency measurement interval.

RTT is the time from when a client sends a message to when it receives the server’s response (or an acknowledgment). For a game running at 60 Hz tick rate, the tick interval is ~16.7 ms. With an RTT of 60 ms, the server-side state a player acts on is already 3–4 ticks stale by the time the player sees it. That is why competitive titles target RTT < 50 ms for ranked play.

Under load, RTT climbs because the server spends more CPU time per tick processing more player inputs before broadcasting. Without RTT, a load test cannot tell a server that handles 1 000 players smoothly from one that processes them with a visible delay.

Jitter is the variance in RTT across successive measurements. With good client-side prediction, a player can adapt to a consistently high latency. Inconsistent latency, such as a 50 ms RTT that swings between 20 ms and 200 ms, breaks prediction and produces visible rubber-banding or teleporting. Jitter is typically expressed as the standard deviation or as the p95−p50 spread of RTT samples.

In a load test, jitter often increases under load even when mean RTT stays low. Garbage collection, I/O or lock contention now and then delays the server’s event loop or thread pool. Jitter metrics expose this intermittent slowness, which mean metrics hide.

For WebSocket-over-TCP tests, true packet loss at the IP layer is handled by TCP retransmission and is not directly visible in the load test. What you observe instead is increased RTT (retransmission adds latency) and occasional TCP connection resets (visible as k6 WebSocket errors). For UDP game servers, packet loss is meaningful and measurable only with a UDP-capable test tool (see Real-time protocols).

Tick rate is a server configuration, not a measurement. It still sets the floor for what your latency metrics mean. A server running at 20 Hz has a 50 ms update interval, so players on a 10 ms network connection still get at most 20 state updates per second. Under load, if the server falls below its target tick rate, ws_session_duration and custom tick latency metrics will show increasing spread.

k6 does not measure WebSocket message RTT automatically. Add a custom Trend metric:

import ws from 'k6/ws';
import { Trend } from 'k6/metrics';
const rtt = new Trend('game_rtt_ms', true); // true = time metric (ms)
const jitter = new Trend('game_jitter_ms', true);
let lastRtt = 0;
export default function () {
ws.connect(
`wss://game.staging.example.com/ws?token=${__ENV.GAME_TOKEN}`,
{},
function (socket) {
// Send an echo-ping every 100ms (10 Hz measurement)
socket.setInterval(() => {
socket.send(JSON.stringify({ type: 'ping', ts: Date.now() }));
}, 100);
socket.on('message', (data) => {
const msg = JSON.parse(data);
if (msg.type === 'pong' && msg.ts) {
const sample = Date.now() - msg.ts;
jitter.add(Math.abs(sample - lastRtt));
lastRtt = sample;
rtt.add(sample);
}
});
socket.setTimeout(() => socket.close(), 120000);
}
);
}

The server must echo the ping message back as a pong with the original ts field preserved. This is a 2–5 line change on the server side.

After adding game_rtt_ms and game_jitter_ms, set thresholds:

export const options = {
thresholds: {
game_rtt_ms: ['p(95)<60', 'p(99)<100'], // competitive-grade targets
game_jitter_ms: ['avg<15'], // jitter average < 15 ms
},
};

MaxoPerf reports threshold violations as run failures. That gives you an automatic pass/fail gate for pre-launch sign-off.

Reading latency percentile charts in MaxoPerf

Section titled “Reading latency percentile charts in MaxoPerf”

After a run, open the Overview tab. The latency chart shows p50, p95, and p99 lines for the primary latency metric. For a WebSocket test, this defaults to ws_connecting (handshake time). To see your custom game_rtt_ms metric, open the Metrics panel and select it.

What healthy looks like:

  • p50 RTT stays flat across the hold phase: the server processes ticks at a consistent speed.
  • Low spread means low jitter: p95 and p99 stay within 2× of p50 throughout the hold phase.
  • No upward drift over the hold duration: the server does not accumulate latency over time.

What degraded looks like:

  • p95 climbs while p50 stays flat. Some ticks are delayed, which points to occasional thread or GC pauses.
  • p99 grows far above p95 because of extreme outliers that often line up with GC or disk I/O.
  • All percentile lines drift upward over 5–10 minutes. The server is slowly falling behind, often because a queue builds up in the game loop.

import Screenshot from ‘@components/Screenshot.astro’;

SymptomLikely causeWhere to look
All percentiles rise togetherServer CPU saturationServer CPU and thread count
p99 spikes while p50 is flatGC pauses or lock contentionServer GC logs, lock profiling
Latency rises after 10+ minutesMemory leak or connection pool leakServer heap, pool exhaustion metrics
Latency high at low VU countUnder-provisioned server baselineInstance sizing, OS socket backlog
Jitter high at any VU countNetwork path variance or batchingServer tick batching config, NIC settings

Do:

  • Add custom RTT and jitter metrics to every game server load test. ws_connecting alone cannot validate game quality.
  • Set thresholds on both p95 and jitter average so the run fails automatically if either degrades.
  • Compare MaxoPerf latency percentile charts with server-side tick-rate telemetry to separate client-visible latency from server-internal processing delay.

Don’t:

  • Use mean (average) RTT as your headline metric. Means hide the outliers that players feel as lag spikes.
  • Measure latency only at connection-time. ws_connecting measures the TCP handshake, not in-game message exchange latency.
  • Accept a clean p95 as final approval without checking the p99 drift over the full hold duration.