Latency, jitter, and packet loss
In game performance testing, “latency” means something more specific and more demanding than the p95 response time that matters for a web API. Players feel RTT in the tens of milliseconds. Jitter — 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 the metrics that matter for game health under load and how to capture and read them in MaxoPerf.
Before you start
Section titled “Before you start”- Read Game server load testing — the game server load test is where these metrics are captured.
- 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.
The metrics that matter for game quality
Section titled “The metrics that matter for game quality”Round-trip time (RTT)
Section titled “Round-trip time (RTT)”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. An RTT of 60 ms means the server-side state a player acts on is already 3–4 ticks stale by the time the player sees it — which 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. A load test that does not measure RTT cannot distinguish between a server that is handling 1 000 players gracefully and one that is processing them with a visible delay.
Jitter
Section titled “Jitter”Jitter is the variance in RTT across successive measurements. A player can adapt to a consistently high latency with good client-side prediction. Inconsistent latency — 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 — the server’s event loop or thread pool is occasionally delayed by garbage collection, I/O, or lock contention. Jitter metrics expose this intermittent slowness that mean metrics hide.
Packet loss
Section titled “Packet loss”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
Section titled “Tick rate”Tick rate is a server configuration, not a measurement — but it sets the floor for what
your latency metrics mean. A server running at 20 Hz has a 50 ms update interval; players
on a 10 ms network connection will still experience 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.
Measuring RTT in a k6 WebSocket test
Section titled “Measuring RTT in a k6 WebSocket test”k6 does not measure WebSocket message RTT automatically — you 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 will report threshold violations as run failures, giving 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 flat across the hold phase — the server is processing ticks at consistent speed.
- p95 and p99 are within 2× of p50 — low spread means low jitter.
- No upward drift over the hold duration — the server is not accumulating latency over time.
What degraded looks like:
- p95 climbing while p50 stays flat — a fraction of ticks are delayed, indicating occasional thread or GC pauses.
- p99 growing far above p95 — extreme outliers; often correlated with GC or disk I/O.
- All percentile lines drifting upward over 5–10 minutes — the server is slowly falling behind, often due to a queue building up in the game loop.
import Screenshot from ‘@components/Screenshot.astro’;
What to fix when latency climbs
Section titled “What to fix when latency climbs”| Symptom | Likely cause | Where to look |
|---|---|---|
| All percentiles rise together | Server CPU saturation | Server CPU and thread count |
| p99 spikes while p50 is flat | GC pauses or lock contention | Server GC logs, lock profiling |
| Latency rises after 10+ minutes | Memory leak or connection pool leak | Server heap, pool exhaustion metrics |
| Latency high at low VU count | Under-provisioned server baseline | Instance sizing, OS socket backlog |
| Jitter high at any VU count | Network path variance or batching | Server tick batching config, NIC settings |
Do / don’t
Section titled “Do / don’t”Do:
- Add custom RTT and jitter metrics to every game server load test —
ws_connectingalone is not sufficient for game quality validation. - Set thresholds on both p95 and jitter average so the run fails automatically if either degrades.
- Correlate MaxoPerf latency percentile charts with server-side tick-rate telemetry to distinguish client-visible latency from server-internal processing delay.
Don’t:
- Use mean (average) RTT as your headline metric — means hide outliers that players experience as lag spikes.
- Measure latency only at connection-time —
ws_connectingmeasures 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.
Where to go next
Section titled “Where to go next”- Game server load testing — the test that generates these metrics.
- Network emulation conditions — test under degraded networks to model real player conditions.
- Launch spike and soak — watch latency under launch-day load and overnight soak.
- By engine: WebSocket load testing — full k6 WebSocket metric reference.