Game server operators obsess over tickrate, CPU clock and RAM, then publish a number that means very little: average tick duration. A 64-slot CS2 server can show a 4.1 ms average tick and still feel worse than a 6 ms server to competitive players, because what players feel is the distribution — specifically the 95th and 99th percentile of tick time under real load, and whether the server is stable enough to tick at its target rate at all. This article lays out a measurement method that maps player count to tick-time percentiles, and explains how to read the result so you buy the right dedicated hardware for a measured player load instead of guessing.
Define the Metric Before You Measure It
- Tick duration: wall time from tick start to tick end. Record every value, not the mean.
- Tick overrun: ticks per minute whose duration exceeds 1000/tickrate ms.
- Simulation debt: cumulative missed simulation time; the source of the “catch-up” snapcraft players see after a spike.
- Stress percentiles: p50, p95, p99 and max per 5-minute window at a fixed player count.
Average tick hides everything because a single 80 ms GC pause among 15,000 sub-millisecond ticks is invisible in the mean but visible to every player in the fight.
Instrumentation That Does Not Lie
Source-engine servers report tick timing through stats and the fps_max-related var; Paper exposes MSPT; others need a wrapper. Whatever the game, sample at the process level too, so you capture kernel and I/O contributions the game timer may not:
# per-second scheduler delay for the server's main thread
pidstat -t -p $(pgrep -f srcds_linux) 1 60 | awk '$5=="UID"{t=$0} {print}'
Pair that with /proc/<pid>/schedstat field 3 (time waiting on a runqueue). If runqueue wait exceeds 100 µs at p99, the host is oversubscribed or the thread is migrating across NUMA nodes — a hardware and pinning issue, not a game-code one.
Method: Load in Steps, Hold, Subtract
A useful curve needs stable plateaus, not a ramp. For each point:
- Start empty, warm the map for 10 minutes (bots idle, or real players in spectator).
- Join synthetic or real clients in one step to the target count (16, 32, 48, 64…).
- Hold 15 minutes of representative activity — combat, movement, ability spam, entity-heavy scenes.
- Discard the first 2 minutes, then record p50/p95/p99/max tick duration and overruns.
- Repeat twice; if p99 varies more than 20% between runs, the host is noisy.
Use scripted input so every run is comparable. A synthetic bot that stands still produces a curve that will not match your real players.
Reading a Real Curve
Results from a 128-tick CS2 instance on a 3.7 GHz all-core, 8-core dedicated host, competitive 5v5 with bots filling the remaining slots (millisecond tick durations):
| Slots | p50 | p95 | p99 | Max | Overruns/min |
|---|---|---|---|---|---|
| 10 | 1.8 | 2.4 | 3.1 | 6.2 | 0 |
| 20 | 2.6 | 3.5 | 4.8 | 9.4 | 0 |
| 32 | 3.4 | 4.9 | 7.1 | 14.8 | 2 |
| 48 | 4.5 | 6.8 | 10.9 | 23.1 | 19 |
| 64 | 5.9 | 9.2 | 16.4 | 41.0 | 97 |
The p50 never exceeds the 7.81 ms budget even at 64 slots — that is why average-tick marketing works. The p99 crosses the budget at 48 slots and doubles it at 64, and overruns appear at 32. The honest capacity of this host at 128 tick is around 36–40 slots. At 64 tick the same host comfortably serves 64 slots, which is the actual business decision: fewer, faster ticks or more slots per box.
Turning Percentiles Into Hardware
Tick duration scales with entity count and player input volume, both roughly linear in players above a threshold, then superlinear when a single thread saturates. Three levers change the curve:
- Single-thread clock: nearly linear effect on tick duration. Going from 3.7 to 4.5 GHz typically cuts p95 by 18–25%. This is usually the highest-leverage purchase for a full instance.
- Core isolation: pinning the main thread and its worker pool to isolated cores (
isolcpusor cgroup cpuset) removes runqueue wait and reduces p99 by 30–50% at identical p50 in our tests. That gap is invisible in average tick and very visible in game. - Memory locality: one socket, one NUMA node, threads on the same L3. Cross-node access adds 40–80 ns per miss and shows up as p99 fattening at high slot counts.
RAM capacity rarely matters for CS2; latency does. Run DDR5 in matched channels, enable ECC, disable power-saving C-states (see our kernel tuning notes) and keep the NIC interrupt on a dedicated core.
Comparing Games Fairly
The same host produces completely different curves per game, so a single “slots supported” number across your fleet is misleading. In our measurements on one 8-core dedicated host, the p99 tick duration at 32 players was approximately 3 ms for CS2 at 64 tick, 11 ms for Rust (71 ms budget at 14 tick), and 47 ms for a vanilla Minecraft world at 20 tick. By overrun count, Minecraft had the most headroom and Rust the least, even though Rust looks like it has the loosest budget.
The reason is variance, not average: Rust’s AI and physics spikes create heavy tails that do not scale linearly with players, while Minecraft’s tick cost per player is comparatively smooth. When you build a fleet table, record p99 as a percentage of the tick budget for each game — that normalises across tickrates and makes the column comparable:
| Game | Tick budget | p99 at 32 players | p99 / budget |
|---|---|---|---|
| CS2 (64 tick) | 15.6 ms | 3.1 ms | 20% |
| Rust (14 tick) | 71.4 ms | 11.4 ms | 16% |
| Minecraft (20 tick) | 50 ms | 47 ms | 94% |
This normalised view flips the conclusion: Minecraft is the node most at risk at 32 players, not Rust, because its p99 is already at the budget even though its absolute latency looks acceptable. Use it when deciding which game gets the faster cores.
Publish a capacity table with three columns per game: comfortable slots, stretch slots (p99 within 1.5× budget), and overload (overruns > 30/min). Track the curve every patch — a single update can shift p99 by 20% — and re-run before every wipe or season launch. Alert on overruns per minute rather than average tick; it is the only metric that correlates with player complaints in our incident reviews.
Finally, confirm the host gives you exclusive cores. If lscpu shows hyperthread siblings shared with another container or your runqueue wait spikes on a quiet server, the platform is the problem and no amount of tuning will fix the p99. Compare dedicated servers with full-core allocation and predictable clocks, then re-run the same test method on the new hardware — the curve, not the spec sheet, tells you what you bought.

Leave a Reply
You must be logged in to post a comment.