Skip to content

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.

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.

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:

  1. A custom k6 xk6 extension compiled with UDP support (not available in the standard MaxoPerf runner image).
  2. A dedicated UDP load-testing tool (e.g. iperf3, netperf, or a game-engine-specific bench tool) running on infrastructure you provision.
  3. Testing the HTTP/WebSocket control plane separately and using UDP simulation tooling for the data plane.
ProtocolMaxoPerf supportNotes
WebSocket (ws://, wss://)Fullk6 k6/ws module; long-lived connections, bidirectional messages, custom metrics
HTTP/1.1 and HTTP/2Fullk6 k6/http; REST, gRPC-web, SSE
gRPC (HTTP/2)Fullk6 k6/net/grpc module
TCP (raw)PartialJMeter TCP Sampler in a .jmx entrypoint
UDPNot nativelyWorkarounds described below
QUIC / HTTP/3ExperimentalServer-dependent; test via k6 HTTP if server negotiates

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

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.

SituationRecommended approach
Browser/mobile game via WebSocketk6 on MaxoPerf (full support)
HTTP REST backend (auth, inventory, leaderboard)k6 HTTP on MaxoPerf
gRPC game servicesk6 gRPC module on MaxoPerf
Raw TCP dedicated serverJMeter TCP Sampler .jmx on MaxoPerf
UDP game server data planeSeparate UDP tool + MaxoPerf for control plane
QUIC/HTTP3 (if server supports)k6 HTTP on MaxoPerf (experimental)