Real-time protocols — WebSocket, TCP, and UDP for games
The network protocol shapes 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 workarounds for protocols outside that set.
Protocol options for game backends
Section titled “Protocol options 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.
- Every browser has the WebSocket API. No plugin or native app is required.
- The protocol handles framing and masking, so application code works with messages, not streams.
- TLS (
wss://) is transparent and uses 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 has lower latency than WebSocket when 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 it needs a JMX file with the sampler configured for your protocol’s framing. MaxoPerf supports this, and it takes more authoring effort than the k6 WebSocket API.
Games use UDP for latency-critical, loss-tolerant 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 basic reason: standard load-testing tools (including k6, JMeter, Taurus and the MaxoPerf runner fleet) operate at the TCP/HTTP/WebSocket layer. To generate meaningful UDP load, you need one of these:
- 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 that use WebSocket, k6 on MaxoPerf is the simplest approach. This 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. Players feel this as “lag.”
UDP workarounds
Section titled “UDP workarounds”To load-test a UDP game server, pick one of these approaches:
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. Then validate UDP data-plane capacity with a smaller, dedicated tool (e.g. a custom Go/Rust UDP flood tool running on a single VM) that measures raw packet handling instead of 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.