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.
Before you start
Section titled “Before you start”- 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.
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. 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
Section titled “Jitter”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.
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. 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.
Measuring RTT in a k6 WebSocket test
Section titled “Measuring RTT in a k6 WebSocket test”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’;
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 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_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.