Tuning Ubuntu 24.04 for Game Servers: Kernel, CPU Governor, and I/O Settings That Move Tick Rate

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:

SettingDefault (ondemand)performance governorEffect on 20 TPS Minecraft
P99 tick time58 ms41 ms-29%
Ticks over 50 ms / hour41297-76%
Idle package power64 W71 W+7 W
Max observed core clock3.6 GHz4.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 taskset
  • kernel.sched_min_granularity_ns = 10000000 — gives each runnable thread a longer uninterrupted slice
  • kernel.sched_wakeup_granularity_ns = 15000000 — reduces wake-up preemption of a running game thread
  • Apply persistently via /etc/sysctl.d/99-game.conf; verify with sysctl -p and cat /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:

ParameterDefaultGame host valueWhy
net.core.rmem_max21299216777216Absorbs UDP bursts without drops
net.core.wmem_max21299216777216Matches send-side queue depth
net.core.netdev_max_backlog100030000Survives player-spike packet storms
net.ipv4.udp_mem~4 MB262144 524288 1048576Raises the UDP memory ceiling
net.ipv4.tcp_fastopen03Cuts 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: none for NVMe (the device already reorders), mq-deadline for SATA SSD. Setting none on a SATA disk hurts; setting cfq on 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: madvise for Minecraft JVM, always for 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 2 or systemd CPUAffinity=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 vcpupin when 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