Real-time protocols — WebSocket, TCP, and UDP for games
The choice of network protocol shapes every aspect of how a game performs under load — and how you test it. This page explains the trade-offs between WebSocket, raw TCP, and UDP for game backends, what MaxoPerf and k6 support natively, and the practical workarounds when you need to test protocols outside that envelope.
Protocol landscape for game backends
Section titled “Protocol landscape for game backends”WebSocket
Section titled “WebSocket”WebSocket is a full-duplex, message-oriented protocol built on TCP. The connection starts as
an HTTP/1.1 upgrade handshake (the 101 Switching Protocols response) and then becomes a
persistent, low-overhead binary or text channel.
Games and real-time web applications use WebSocket because:
- It works through firewalls and proxies that block raw TCP on arbitrary ports.
- The browser WebSocket API is universally available — no plugin, no native app required.
- Framing and masking are handled by the protocol, so application code works with messages, not streams.
- TLS (
wss://) is transparent — the same TLS termination as HTTPS.
WebSocket is the right choice for browser-based games, mobile games that go through an HTTP proxy, and any real-time service that must traverse corporate firewalls.
TCP (raw)
Section titled “TCP (raw)”Raw TCP is lower latency than WebSocket when stripping the HTTP upgrade overhead matters (typically < 100 μs), and it gives the server full control over framing. Game engines like Valve’s Source SDK, most dedicated game-server frameworks, and authoritative simulation servers often use raw TCP on a custom port.
Load-testing raw TCP requires a custom k6 extension or a JMeter TCP sampler. MaxoPerf runs standard k6 and Taurus/JMeter — the JMeter TCP Sampler works, but requires a JMX file with the sampler configured for your protocol’s framing. This is supported but requires more authoring effort than the k6 WebSocket API.
UDP is the protocol of choice for latency-critical, loss-tolerant game traffic: player position updates, hit detection, real-time audio, and any payload where a dropped packet is better than a delayed one. Most competitive first-person shooters, racing games, and battle royale titles use a custom UDP-based protocol (often QUIC, ENet, or a bespoke reliability layer on top of raw UDP).
UDP load testing is hard for a fundamental reason: standard load-testing tools — including k6, JMeter, Taurus, and the MaxoPerf runner fleet — operate at the TCP/HTTP/WebSocket layer. Generating meaningful UDP load requires either:
- A custom k6 xk6 extension compiled with UDP support (not available in the standard MaxoPerf runner image).
- A dedicated UDP load-testing tool (e.g.
iperf3,netperf, or a game-engine-specific bench tool) running on infrastructure you provision. - Testing the HTTP/WebSocket control plane separately and using UDP simulation tooling for the data plane.
What MaxoPerf and k6 cover natively
Section titled “What MaxoPerf and k6 cover natively”| Protocol | MaxoPerf support | Notes |
|---|---|---|
WebSocket (ws://, wss://) | Full | k6 k6/ws module; long-lived connections, bidirectional messages, custom metrics |
| HTTP/1.1 and HTTP/2 | Full | k6 k6/http; REST, gRPC-web, SSE |
| gRPC (HTTP/2) | Full | k6 k6/net/grpc module |
| TCP (raw) | Partial | JMeter TCP Sampler in a .jmx entrypoint |
| UDP | Not natively | Workarounds described below |
| QUIC / HTTP/3 | Experimental | Server-dependent; test via k6 HTTP if server negotiates |
WebSocket load test for a game backend
Section titled “WebSocket load test for a game backend”For browser-based or mobile games using WebSocket, k6 on MaxoPerf is the most productive approach. The following minimal script models one player session:
import ws from 'k6/ws';import { check } from 'k6';import { Trend } from 'k6/metrics';
const tickLatency = new Trend('game_tick_latency_ms');
export const options = { vus: 200, duration: '5m', thresholds: { ws_connecting: ['p(95)<200'], game_tick_latency_ms: ['p(95)<80'], },};
export default function () { const res = ws.connect( `wss://game.staging.example.com/ws?token=${__ENV.GAME_TOKEN}`, {}, function (socket) { socket.on('open', () => { socket.send(JSON.stringify({ type: 'join', room: 'arena-1' })); });
socket.on('message', (data) => { const msg = JSON.parse(data);
// Measure server-to-client tick latency if (msg.type === 'world_state' && msg.server_ts) { tickLatency.add(Date.now() - msg.server_ts); }
check(msg, { 'no error event': (m) => m.type !== 'error' });
// Acknowledge every world-state tick if (msg.type === 'world_state') { socket.send(JSON.stringify({ type: 'ack', seq: msg.seq })); } });
socket.on('error', (e) => { check(null, { 'no ws error': () => false }); });
// 5-minute session socket.setTimeout(() => socket.close(), 300000); } );
check(res, { 'ws handshake 101': (r) => r && r.status === 101 });}Upload this as the k6 entrypoint in MaxoPerf. The custom game_tick_latency_ms trend
metric measures the game-specific server-push latency — the time from when the server
generates a world state to when the client receives it — which is what players actually
experience as “lag.”
UDP workarounds
Section titled “UDP workarounds”When you must load-test a UDP game server, the practical approaches are:
Approach 1 — Test the control plane separately. Most UDP game servers have an HTTP or WebSocket control plane for matchmaking, session allocation, and authentication. Test that control plane at full load in MaxoPerf. UDP data-plane capacity is then validated with a smaller, dedicated tool (e.g. a custom Go/Rust UDP flood tool running on a single VM) that measures raw packet handling, not session-level load.
Approach 2 — Proxy shim. Build a thin WebSocket proxy that wraps your UDP game protocol. Load-test the WebSocket proxy in MaxoPerf at target concurrency, then extrapolate UDP capacity from the proxy’s CPU and memory profile.
Approach 3 — Game engine built-in bench. Many game engines ship with a headless bench client (e.g. Unreal’s dedicated server bench mode). Use MaxoPerf to orchestrate the REST/WebSocket workload and run the engine bench separately on provisioned VMs.
Protocol decision guide
Section titled “Protocol decision guide”| Situation | Recommended approach |
|---|---|
| Browser/mobile game via WebSocket | k6 on MaxoPerf — full support |
| HTTP REST backend (auth, inventory, leaderboard) | k6 HTTP on MaxoPerf |
| gRPC game services | k6 gRPC module on MaxoPerf |
| Raw TCP dedicated server | JMeter TCP Sampler .jmx on MaxoPerf |
| UDP game server data plane | Separate UDP tool + MaxoPerf for control plane |
| QUIC/HTTP3 (if server supports) | k6 HTTP on MaxoPerf (experimental) |
Where to go next
Section titled “Where to go next”- Game server load testing — build the first concurrent-player test.
- By engine: WebSocket load testing — full k6 WebSocket API.
- Latency, jitter, and packet loss — measure the metrics that matter to players.
- k6 scripts on Maxoperf — upload and run workflow.