Modern game servers rarely use every core you give them — most run their simulation on one or two threads while the rest of the box idles. On a shared or multi-instance machine, the OS scheduler can bounce those hot threads between cores, dragging cache and latency with them. CPU pinning (affinity) and NUMA awareness fix exactly that: they lock threads to specific cores and keep memory access local, which translates directly into steadier tick rates and lower worst-case ping. It is one of the cheapest performance upgrades available, because it costs nothing but configuration time.
When Pinning Actually Helps
- Multiple game instances on one box — each server gets its own cores, so a busy Minecraft world cannot steal CPU from a Rust server on the same machine.
- Latency-sensitive simulations — single-thread games like Project Zomboid or ARK benefit when the main thread never migrates.
- Dedicated boxes with background services — backups, monitoring agents, and databases get their own cores instead of competing.
Skip pinning on a single-instance server with plenty of idle cores — the scheduler already keeps the hot thread resident, and you gain nothing but complexity. Pinning is a fix for contention, not a free speed boost.
Checking Your Topology First
Before pinning anything, find out how cores and memory are laid out. On Linux:
lscpu -e # core/socket/NUMA mapping per CPU
lscpu | grep NUMA # NUMA node layout
taskset -pc $$ # current affinity of a shell
A two-socket EPYC or Xeon box has two NUMA nodes, each with its own memory. Threads that run on node 0 but allocate memory on node 1 pay a cross-node penalty — small per access, but it adds up at thousands of ticks per minute. Single-socket machines have one node and none of these concerns, which is why many game hosts prefer them despite the lower core ceiling.
Pinning with taskset and systemd
For a quick test, launch the server pinned to cores 0–1:
taskset -c 0,1 ./server.sh start
For something that survives reboots, use a systemd unit with CPUAffinity:
[Service]
ExecStart=/srv/games/minecraft/start.sh
CPUAffinity=0,1
MemoryMax=8G
Or pin by PID at runtime with taskset -p -c 0,1 $(pgrep -f java). To pin all threads of a multithreaded server, walk /proc/PID/task/* and set affinity per thread — a short loop does it:
for t in /proc/$(pgrep -f server)/task/*; do
taskset -p -c 0,1 ${t##*/}
done
Remember that core numbers from lscpu -e are physical IDs; on a hyperthreaded CPU, cores 0 and 1 may share a physical core. If you pin two busy instances to sibling threads, you get contention instead of isolation — check the CPU column of lscpu -e and give each instance its own physical core.
NUMA-Aware Memory Placement
Pinning CPU without pinning memory gets you half the benefit. Use numactl to keep the process’s memory on the same node as its cores:
numactl --cpunodebind=0 --membind=0 ./server.sh start
On a single-socket box there is one node, so this is a no-op — but on dual-socket hardware, membind prevents the “local CPU, remote RAM” pattern that shows up as erratic lag spikes. Check allocation with numastat -p PID and /proc/PID/numa_maps. If the game allocates a huge world buffer at startup, keep it on the same node as the threads that touch it every tick.
Realistic Expectations
| Scenario | Expected change |
|---|---|
| Two game servers on one box, cores split 4+4 | Worst-case tick spikes drop 30–50%; average tick time unchanged |
| Single server, idle box | No measurable difference |
| Dual-socket, cross-node traffic eliminated | P99 latency improves; average latency roughly flat |
The pattern to notice: pinning smooths the tail, not the average. Your players will feel it as fewer lag spikes during raids and server-wide events, not as a lower baseline ping. That is exactly the profile a gaming community notices.
Pitfalls to Avoid
- Pinning to logical (HT) siblings — two busy threads on one physical core contend for execution units; verify with
lscpu -e. - Over-pinning — leaving the OS with no cores for interrupts, SSH, and monitoring causes system-wide stalls. Reserve at least one core for the kernel.
- Pinning before measuring — capture baseline tick times with
top -Hor a tick-rate mod first; otherwise you cannot tell if the change helped. - Ignoring cgroup limits — if you run containers, set
--cpuset-cpusat the container level instead of relying on in-container taskset.
Pinning is a tuning step, not a substitute for correct sizing. If the box is under-powered, affinity just makes the shortage more consistent. Pair it with real capacity planning — and if you would rather have a host that handles the CPU layout for you, bare-metal game server hosting gives you full BIOS and scheduler control without shared-neighbor noise. Start with the baseline measurement, pin one instance, compare the tail latency, and only then roll the change out to the rest of your fleet.




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