Skip to content

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.

  • 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.

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 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.

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 — 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.

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’;

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 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_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.