--- title: "Pinning HLDS to a CPU Core with taskset" excerpt: "How to pin an HLDS or ReHLDS instance to a specific CPU core with taskset for better cache locality, and why it matters most when packing many servers on one box." category: "optimization" tags: "cpu, affinity" image: "" read_time: "8 min read" ---
By default the Linux scheduler is free to bounce your HLDS process from core to core. For a single-threaded, latency-sensitive game server that is mildly wasteful — every migration means a cold cache on the new core — and on a box running several servers it is genuinely harmful, because instances end up fighting over the same cores while others sit idle. taskset fixes both by pinning each server to a core you choose. Here is how to use it correctly.
Know how many cores you have and how they are numbered before pinning:
nproc lscpu | grep -E 'CPU\(s\)|Thread|Core'
Cores are numbered from 0. On a 4-core box you have 0,1,2,3. Note whether hyper-threading is on — sibling threads share a physical core, so pinning two busy servers to two siblings is not the same as two real cores.
Wrap the launch command with taskset -c and the core number:
taskset -c 2 ./hlds_run -game cstrike +ip 0.0.0.0 +port 27015 +map de_dust2 +maxplayers 32
-c 2 confines the process (and its threads) to core 2. Everything the server spawns inherits that affinity. Reserve core 0 for the OS and interrupts where you can, and give servers cores 1 and up.
You do not have to restart to pin. Find the PID and set affinity live with -p:
pgrep -a hlds taskset -cp 2 12345 # 12345 = the hlds_linux PID
Read the current affinity back the same way without a core list:
taskset -cp 12345 # pid 12345's current affinity list: 2
Note that the real game process is often hlds_linux under the hlds_run wrapper — pin the actual hlds_linux PID, not just the shell wrapper.
Two concrete wins:
The effect is small for a single lightly-loaded server and large for a dense multi-server box — that is where taskset earns its keep.
Pinning the game to a core only helps if that core is not also being hammered by network interrupts. On a busy server the NIC generates a lot of hardware interrupts, and if those land on the same core as your server they compete for it. Two complementary moves keep them apart. First, dedicate a core (or the OS default core 0) to interrupts and keep your servers off it. Second, on multi-queue NICs you can steer the interrupts explicitly by writing an affinity mask to each queue's IRQ:
grep eth0 /proc/interrupts # find the NIC's IRQ numbers cat /proc/irq/<irq>/smp_affinity_list
The principle is simple: let one core handle NIC interrupts and the OS, and give each game server its own separate core. That way the server's core spends its time on the game loop and nothing else — which is the entire point of pinning in the first place.
Affinity set with taskset -cp is lost when the process restarts, so bake it into how the server launches. Either prefix the launch command in your start script with taskset -c N, or if you run under systemd use the built-in directive:
# in the [Service] section of your unit file CPUAffinity=2
systemd then pins the service to core 2 every start, no wrapper needed. Reload with systemctl daemon-reload and restart the unit.
hlds_run is a shell script; confirm the actual hlds_linux PID carries the affinity.-cp is runtime-only. Put taskset in the launch command or use CPUAffinity=.Confirm the pin took and holds. Read affinity back with taskset -cp <pid> — it should list exactly the core you set. Then run top, press 1 for per-core view, and watch under load: the server's CPU should appear on your chosen core and stay there instead of hopping between cores. On a multi-server box, confirm each instance sits on its own core with none doubled up. Pair this with the performance governor so the pinned core also runs at full clock, and with a fast single-thread CPU so that core is worth pinning to.