A stock Ubuntu 24.04 install runs a game server acceptably. It does not run one well. The gap between a default kernel and a tuned one shows up as tick spikes, not average FPS — which is why so many operators blame the host when the real problem is a CFS scheduler sharing a game thread with a cron job.
This is a working configuration for Linux game hosts: kernel parameters, CPU governor, filesystem, and I/O scheduler settings, with the numbers each one moves. All values below are measured on a Ryzen 9 7950X node running Minecraft Java, Palworld, and ARK: Survival Ascended under load.
Start with the kernel and governor, not the game
Two settings do most of the work. The first is the CPU frequency governor. Most providers ship powersave or ondemand, which ramp frequency based on utilization sampled every few milliseconds. A game server thread that alternates between busy and idle every tick never triggers the ramp properly, so you pay a latency penalty on the first tick after each idle window.
Setting the governor to performance on the cores that host game processes removed a measurable share of tick overshoot in our tests:
| Setting | Default (ondemand) | performance governor | Effect on 20 TPS Minecraft |
|---|---|---|---|
| P99 tick time | 58 ms | 41 ms | -29% |
| Ticks over 50 ms / hour | 412 | 97 | -76% |
| Idle package power | 64 W | 71 W | +7 W |
| Max observed core clock | 3.6 GHz | 4.5 GHz | +25% |
The second is the CFS scheduler’s migration cost. By default the kernel balances threads across cores aggressively, and it will happily migrate a hot game thread to a colder core mid-tick, invalidating its L2/L3 cache. Raising sched_migration_cost_ns makes the scheduler leave busy threads where they are.
kernel.sched_migration_cost_ns = 5000000— five times the default; keeps hot threads pinned without manual tasksetkernel.sched_min_granularity_ns = 10000000— gives each runnable thread a longer uninterrupted slicekernel.sched_wakeup_granularity_ns = 15000000— reduces wake-up preemption of a running game thread- Apply persistently via
/etc/sysctl.d/99-game.conf; verify withsysctl -pandcat /proc/sys/kernel/sched_migration_cost_ns
Network buffers sized for packet bursts, not throughput
Game traffic is small packets at high frequency. Default socket buffers are sized for bulk transfer and overflow during a launch night. The receive buffer is the one that matters most on a game host:
| Parameter | Default | Game host value | Why |
|---|---|---|---|
| net.core.rmem_max | 212992 | 16777216 | Absorbs UDP bursts without drops |
| net.core.wmem_max | 212992 | 16777216 | Matches send-side queue depth |
| net.core.netdev_max_backlog | 1000 | 30000 | Survives player-spike packet storms |
| net.ipv4.udp_mem | ~4 MB | 262144 524288 1048576 | Raises the UDP memory ceiling |
| net.ipv4.tcp_fastopen | 0 | 3 | Cuts handshake RTT for TCP-based admin panels |
If you run a single high-population node and want kernel-level latency control beyond sysctl, see the trade-offs in our dedicated server comparison table — bare-metal access is required for the kernel bypass techniques that start where these tunables stop.
Filesystem, I/O scheduler, and swap
- Filesystem: XFS for world saves. It handles large sequential writes and concurrent metadata better than ext4 on NVMe under modded Minecraft save loads.
- Mount options:
noatime,nodiratime,discard=async. Disabling access-time updates removes a write per read on every chunk load. - I/O scheduler:
nonefor NVMe (the device already reorders),mq-deadlinefor SATA SSD. Settingnoneon a SATA disk hurts; settingcfqon NVMe adds latency for no gain. - Swap: keep 2-4 GB as a safety net but set
vm.swappiness=10. A game server that swaps is already failing; you want a buffer, not routine paging. - Transparent hugepages:
madvisefor Minecraft JVM,alwaysfor Palworld and ARK which allocate large linear heaps.
To verify each change actually landed, read back the live value instead of trusting the config file — and check the I/O scheduler per device with cat /sys/block/nvme0n1/queue/scheduler, since the default can differ between NVMe, SATA, and virtual block devices.
Verifying the result instead of assuming it
Measure before and after with the same load. The metrics that matter are P99 tick time, ticks over budget per hour, and packet drops at the socket layer. A tuned node should hold 20 TPS with under 100 over-budget ticks per hour at 200 players; if it does not, the bottleneck is CPU single-thread performance or uplink, not the kernel.
Read the counters directly: netstat -su for UDP receive errors, /proc/net/softnet_stat for dropped packets in the second column, and nstat -az for a full snapshot. Rising softnet drops mean the network stack, not the game, is the limiter.
CPU pinning and IRQ placement
Kernel tunables change how the scheduler treats game threads; they do not stop it from putting your tick thread on the same core that is servicing network interrupts. On a node with a busy uplink, that collision shows up as periodic latency spikes roughly every few hundred milliseconds, matching the interrupt rate.
- Enable
irqbalance, then exclude the game cores from its mask so it never migrates an interrupt onto them - Pin the tick thread with
taskset -c 2or systemdCPUAffinity=2, and keep core 0 for IRQs and the OS - For multi-instance nodes, give each instance a fixed core set to prevent two tick threads landing on one core
- Use
virsh vcpupinwhen the server is virtualized — the guest scheduler cannot fix host-side contention
Pin the memory controller too, not just the CPU. On a dual-socket node, a thread on socket 1 reading RAM attached to socket 0 crosses the inter-socket link on every miss, adding 60-90 ns per access. NUMA-aware placement (numactl --cpunodebind=0 --membind=0) removes it, and on a game server that latency appears directly in the tick budget.
What to leave alone
Not every default is wrong. Disabling C-states entirely raises idle power for a gain you will not measure on a game server that is already loaded. Transparent hugepages set to always help large Java heaps but can add tail latency to allocation-heavy workloads; measure before switching. And do not chase exotic filesystem or stack options you cannot roll back — a game host that boots to a broken network stack is worse than one running at 97% of its potential.
Change one thing at a time
Apply the scheduler and governor changes first, measure, then network buffers, then filesystem and I/O. Changing six settings at once and seeing an improvement tells you nothing about which one worked, and nothing about which one to revert when the next workload behaves differently. Keep each change in its own drop-in file under /etc/sysctl.d/ so it can be removed independently.
Match these tunables to hardware that can actually use them. A 96-core node with 2.5 GHz cores will lose to an 8-core node at 4.5 GHz on single-threaded game logic; see the full specs and pricing for single-thread ratings before you tune anything else.



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