Game Server Lag Troubleshooting and Capacity Planning Playbook: The 60-Second Sweep

The message is always the same: “the server was fine yesterday and now everyone is lagging.” Yesterday is not a useful data point. What separates an operator who fixes this in twenty minutes from one who flails for two days is a fixed diagnostic sequence that converges regardless of the symptom. This is that sequence, built for game servers and high-traffic web nodes, with the actual commands and the thresholds that tell you where to stop looking.

Step 0: Classify the Symptom Before Touching Anything

SymptomMost likely layerFirst check
Uniform lag for all players, all regionsCPU saturation or tick overrunturbostat, mpstat -P ALL 1
Lag only for players in one regionNetwork path or transitmtr from that region to your IP
Periodic stutter every N secondsI/O wait or a cron/backup jobiostat -x 1, crontab -l
Sudden packet loss, no CPU spikeUpstream congestion or attackProvider traffic graphs, nstat -az
Site slow, server idleApplication layer / database / external APISlow query log, APM traces
Works for hours, then diesMemory leak or connection exhaustionRSS trend, ss -s

This table alone eliminates about 70% of wasted investigation time. The critical discipline is refusing to check anything outside the indicated layer until that layer is cleared.

Step 1: The Sixty-Second Sweep

Run these six commands in order. Total runtime under a minute, and it localizes the problem in the overwhelming majority of cases.

uptime — load average relative to core count. Load of 24 on 32 cores is healthy; load of 24 on 8 cores is saturation.
vmstat 1 10 — watch the r (run queue) and wa (I/O wait) columns. Persistent r above your core count means CPU-bound. wa above 10% means storage-bound.
mpstat -P ALL 1 5 — look for one core at 100% while others idle. That is a single-threaded bottleneck, extremely common in game servers.
iostat -x 1 5 — check %util (above 80% is a storage ceiling) and await (above 20 ms on NVMe is abnormal).
free -h and cat /proc/meminfo | grep -i dirty — a high “dirty” value with a stalled flush points at write-back blocking.
ss -s — TIME_WAIT in the tens of thousands indicates a connection-churn problem, common on game servers with short session lifetimes.

Step 2: Game-Specific Diagnostic Thresholds

MetricHealthyWarningPlayer-visible failure
Minecraft MSPT (ms per tick)< 25 ms25-45 ms> 50 ms = visible rubber-banding
Rust server FPS> 6030-60< 30 = hitches and desync
CS2 tick deviation< 1 ms1-3 ms> 3 ms = hit registration complaints
Valheim instance CPU time< 60% of one core per world60-85%> 85% = world sim slows
TCP p99 response (web)< 200 ms200-800 ms> 2 s = abandonment
Disk await on NVMe< 5 ms5-20 ms> 20 ms = query stacking

For a Minecraft network, /spark profiler start (Paper’s built-in Spark plugin) is the correct first tool, not a general profiler. It reports per-plugin and per-entity-type tick contribution directly, which maps to an actionable culprit in one run. For Rust, perf top against the server PID shows whether the cost sits in entity AI, physics, or serialization.

Step 3: Isolate the Layer With a Controlled Test

  1. Stop the game/app processes. If load drops instantly, it is an application problem and you should stay in the application layer. If load persists, you have a system-level problem — cron, backup, or a runaway daemon.
  2. Run a disk test in parallel with the workload. fio --rw=randwrite --bs=4k --numjobs=4 --runtime=60. If application latency spikes with the fio load and not without it, storage is the shared bottleneck.
  3. Test with a hard CPU cap. Run the workload under systemd-run --scope -p CPUQuota=80%. If latency improves when you reduce available CPU, you were suffering from cache thrashing or thermal throttling, not raw CPU shortage — an important and frequently missed distinction.
  4. Bypass the network path. Measure latency from the datacenter itself to the game node. If local latency is fine and player latency is bad, the problem is transit or routing, not the server. At that point escalate to the provider with a traceroute, not to the application config.

Step 4: Capacity Planning From Measurement, Not Guesswork

Once the incident is resolved, convert it into a planning number. Collect the following for two weeks at 1-minute resolution:

  • Peak 1-minute CPU utilization, and the time of day it occurs
  • Peak concurrent players/users
  • p99 tick time or p99 HTTP response
  • Peak egress Mbps
  • Peak IOPS and average write throughput

Then apply these headroom rules. They are deliberately conservative because they are the values that keep you out of the step-zero table:

ResourceTarget peak utilizationWhy
CPU60-70%Game tick time is non-linear near saturation
RAM70-80%Leave room for filesystem cache
NVMe IOPS50-60%Consumer-grade and shared NVMe degrades badly above this
Bandwidth50% of port speedAttacks and spikes need burst headroom
Disk capacity70%Write amplification and snapshot space

If your measured peak exceeds any of these, you are one incident away from a bad night. The dedicated server tier options on the main site list CPU, RAM, and uplink in a directly comparable format, which makes the upgrade decision after a capacity review mechanical rather than speculative.

Apply the same headroom logic to the non-obvious resources. Connection tracking tables, open file descriptors, and worker process counts all have hard ceilings that are invisible until they are hit. A game server with fs.file-max left at the distribution default will fail to accept new players at exactly the moment your network is growing fastest, and the error will look like a network problem rather than a limits problem. Raise nofile to 65535 and net.netfilter.nf_conntrack_max to 1048576 on every node as a matter of course, before you need them.

Step 5: The Preventative Baseline

  • Alert on trends, not just thresholds. A CPU that rose 10 percentage points over 14 days is a bigger signal than one peaking at 90% during a known promotional window. Use Prometheus with a 7-day deriv() alert.
  • Log the incident, not just the fix. Symptom, metric that identified it, root cause, change made, and the metric value after. Six months later this file is the most valuable document your operation owns.
  • Load-test before launch, not after. A synthetic bot run to 1.5× expected peak on the new configuration costs an afternoon and finds the connection ceiling, the memory leak, and the index you forgot — before your players do.
  • Re-baseline after every hardware or kernel change. A kernel upgrade that alters C-state behavior can shift your sustained clock by 10%, and nothing in your monitoring will label that change if you did not record the “before” number.

One operational habit worth building on top of the sweep: keep a short “known-good baseline” record for each node. Ten numbers — sustained all-core clock, idle RAM usage, p99 tick time at 50% load, baseline egress, and the six others you rely on — captured on the day you first benchmark the hardware. When a player reports lag six months later, comparing against that record tells you in thirty seconds whether something has genuinely degraded or whether the workload simply grew. If your baseline record is from a node you have already outgrown, the current dedicated server specifications on the main site give you a fresh comparison point for the next tier up.

Troubleshooting and capacity planning are the same discipline pointed in two directions. The operator who measures continuously plans upgrades before they are urgent; the operator who only measures during an outage spends their evenings in step one. Instrument the node now, and the next “it was fine yesterday” message takes five minutes instead of two days.

Leave a Reply