Skip to content

About

Hardware-aware, host-aware tuning and bottleneck detection for self-hosted Minecraft servers

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

67 Commits

Folders and files

Repository files navigation

tickwarden

Tuning and lag diagnostics for self-hosted Minecraft servers.

tickwarden tunes a server to the hardware it's actually running on. It also looks underneath Java itself to explain lag that in-game profilers can't see: CPU throttling, slow disk access caused by other containers on the same box, OS and kernel settings, and overheating. A profiler like spark tells you what's slow inside the game; tickwarden covers the layers below that.

It's a single Go binary with nothing to install alongside it, plus an optional Fabric mod that reports live TPS and serves a small web dashboard. You can run it in two places:

  • Inside the server's container, to tune the server, line up TPS drops with the container's own resource pressure, and (optionally) auto-scale distance.
  • On the Proxmox or Docker host, to find which container is starving the others — and throttle it.

What sets it apart: its tuning model isn't folklore. Every constant (players × sim², the flying-vs-survival budget, the clustered factor, the capacity cliff) was measured on real hardware — see docs/DECISION_TREE.md.

Prebuilt binaries (Linux amd64/arm64, macOS arm64) and the companion jar are on the releases page.

Quickstart

Starting from nothing? init scaffolds a complete Fabric server that is tuned to your hardware from its very first boot — server jar, the recommended perf mods (lithium, c2me, ferritecore, krypton, scalablelux, spark) at the right versions from Modrinth, a tuned server.properties, a start.sh with sized heap + GC flags, and a systemd unit:

tickwarden init -dir minecraft -mc 1.21.5 -players 6 -accept-eula
sh minecraft/start.sh

Already running a server? Tune it in place:

# 1. tune to your hardware (reads cores, RAM, cgroup limits, installed mods)
tickwarden optimize -mods-dir /path/to/mods

# 2. drop the companion jar in the server's mods/ and restart, then:
#    - live status dashboard:  http://127.0.0.1:9225/
#    - let the server tune itself, watching only (safe):
tickwarden daemon
#    - ...with a web control plane (live status, decision log, knob changes):
tickwarden daemon -ui 127.0.0.1:9226
#    - ...or actually scale simulation distance with load:
tickwarden daemon -apply

# on the Proxmox/Docker host: find and throttle a noisy neighbour
tickwarden host
tickwarden iostorm -apply

Settings live in tickwarden.toml (defaults < config < flags); install the daemon as a service with scripts/install.sh.

Install

go install github.com/fungiblemoose/tickwarden/cmd/tickwarden@latest

Or grab a binary from releases, or build one to drop into a container:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o tickwarden ./cmd/tickwarden

Commands

Provisioning:

tickwarden init -dir minecraft    # scaffold a NEW Fabric server, tuned from first boot
tickwarden init -accept-eula      # ...and accept Minecraft's EULA (your act, never implied)
tickwarden init -skip-downloads   # config files only (offline; fetch jars yourself)

init never accepts the EULA for you, refuses to overwrite an existing server's files, and writes the JVM flags into a start.sh it owns — the "report but never write JVM flags" rule still holds for live servers.

Tuning (run anywhere, including inside the container):

tickwarden optimize               # one-shot: detect + mods + players → the whole plan, one screen
tickwarden optimize -apply        # ...and write the server.properties part (dry-run unless -write)
tickwarden detect                 # hardware + cgroup limits the server runs under
tickwarden tune                   # recommended settings, each with a reason and confidence
tickwarden tune -mods-dir mods/   # read installed mods; flag conflicts, suggest missing ones
tickwarden tune -players 8        # size view/sim distance for the expected peak players
tickwarden tune -apply            # preview writing those settings to server.properties
tickwarden tune -apply -write     # apply (after a .bak backup); -revert undoes it

tune labels every recommendation solid (trust it), heuristic (a sane default), or contested (test it against your own load). Java memory and startup flags are reported but never written for you. Only server.properties keys get applied, and a [lock] section in tickwarden.toml pins any value you want left alone.

In-container observability (TPS needs the companion mod, below):

tickwarden watch -tps-url http://127.0.0.1:9225/tps   # is a TPS dip the world's fault or the host's?
tickwarden bench -tps-url ... -out before.json        # measure a window: TPS/MSPT + pressure
tickwarden bench-diff before.json after.json          # compare two runs
tickwarden hotspots                                   # loaded chunks ranked by block-entity count
tickwarden pregen                                     # Chunky pregen that yields to players/load
tickwarden loadtest                                   # drive a fixed Chunky load while benchmarking

bench-diff marks a comparison inconclusive when host pressure was different between the two runs, because then a TPS change might be the host's fault rather than your change's.

Host-level (run on the Proxmox/Docker host):

tickwarden host        # rank containers by CPU/IO load; name the noisy neighbour
tickwarden iostorm     # detect a write-storm starving the server; suggest a cgroup io.max throttle
tickwarden ostune      # CPU governor, swappiness, THP, I/O scheduler — with the exact fix
tickwarden thermal     # CPU temperature / frequency throttling

The companion mod (companion/)

watch, bench, hotspots, and pregen need the server's real tick health, and that number only exists inside the running game. spark can't hand it over through RCON (its commands reply asynchronously and come back empty), so companion/ is a small Fabric mod that times each server tick and serves the result as JSON on a localhost-only endpoint:

cd companion && ./gradlew build    # needs JDK 21
# drop build/libs/tickwarden-companion-*.jar into the server's mods/, then restart
curl http://127.0.0.1:9225/tps        # {"tps":20.00,"mspt":6.3,"players":0,"players_peak":1}
curl http://127.0.0.1:9225/hotspots   # [{"dimension":"...","x":-2,"z":0,"block_entities":2}]
curl http://127.0.0.1:9225/jvm        # heap + cumulative GC counters (0.6.0+, feeds the GC gate)

It binds 127.0.0.1 only (port 9225, override with -Dtickwarden.port= or TICKWARDEN_PORT). Versions in companion/build.gradle are pinned to Minecraft 1.21.5; bump them for other versions.

How the cross-layer part works

Inside a container you can read your own cgroup-v2 pressure and throttling stats, so you can tell you were starved without needing any host access:

/sys/fs/cgroup/<self>/{cpu,io,memory}.pressure
/sys/fs/cgroup/<self>/cpu.stat        # nr_throttled, throttled_usec

Detecting that you were starved covers most of the value and needs no privileges. Naming the cause, meaning which other container saturated the disk, does need a view of the host. That's what tickwarden host provides.

Two things I learned running this on a real Proxmox LXC, both now handled in code:

  • Read PSI up the cgroup chain, not just the leaf. The workload sits in a /.lxc leaf whose own pressure reads close to zero. The real pressure shows up one level up, at the namespace root. So watch scans from leaf to root and keeps the worst reading.
  • Host-applied CPU caps are invisible from inside. A Proxmox cpulimit is enforced on a cgroup above the container's namespace ceiling, so the in-container throttle counter reads 0 even at 90%+ throttling. PSI still shows the symptom, and tickwarden host reads the real number from the host side.

What it's tuning for

The goal is to get the most render and simulation distance your system can hold at a steady 20 TPS, instead of playing it safe with conservative defaults. The distance rules are calibrated to a ceiling that's actually been measured. View distance gets pushed hard for render range, since that costs bandwidth and RAM rather than tick CPU, and it's cheap once chunks are pregenerated on an SSD. The catch is that when you share a host, tickwarden sizes things to your cgroup's real budget and uses the host and iostorm views so that maxing out your server doesn't starve everything else on the box.

The rules are upfront about how solid they are. Some are well established, some are educated guesses, and the contested ones (like the per-core distance budget) say so outright. Each carries its confidence level and the reasoning behind it, and docs/DECISION_TREE.md records which numbers come from a real measurement and which are extrapolated. bench and bench-diff exist so a contested rule can be settled with data instead of an argument.

Roadmap

Status: v1.0 — the roadmap is complete. Every tier below is built and, where it counts, validated on real hardware. v1.0 also adds the things that make it a product rather than a toolkit: a web dashboard, a daemon that runs the adaptive controller as a service, a config file, and iostorm -apply to auto-throttle a noisy neighbour. See CHANGELOG.md.

Tier What Status
0 Hardware/cgroup-aware tuner with reasons (detect, tune) done
0.5 Apply to server.properties with locks + revert done
1 TPS × cgroup-PSI correlation (watch) done
1.5 Benchmark + diff with host-load confound detection (bench, bench-diff) done; a built-in load driver beyond loadtest is still open
2 Host agent: name the noisy neighbour, read host-applied limits (host, iostorm) done
below-Java OS/kernel (ostune), thermal, mod intelligence, lag-by-location (hotspots), host-aware pregen done
3 Adaptive tuning: scale sim/view distance live with load (adaptive) done — companion changes distance at runtime (no restart); controller sheds fast / raises slow, holds a floor, and pre-sizes for the expected peak so a join never triggers a cut. Opt-in: dry-run by default, -apply to act

Adaptive (tickwarden adaptive)

Scales simulation/view distance to the live load instead of fixing one value for the worst case. It reads the companion's TPS/MSPT, decides with the measured players × sim² model, and (with -apply) changes distance at runtime — no restart, no player kick. Safety: view (render) distance is never lowered — view costs bandwidth and RAM, not tick CPU, so cutting it recovers nothing; only sim sheds load. Plus hard floors, fast-down/slow-up ramps, a deadband, a 50ms panic valve (when ticks are actually being dropped, sim snaps straight to the floor instead of stepping), and on-join pre-sizing — it sizes for your expected peak (players_peak), so a join finds a peak-safe distance already set rather than forcing a drop.

It's also cause-aware, which no in-game distance scaler is. Each decision checks why MSPT is high before acting:

  • Host starvation — cgroup PSI and CPU-throttling (the same signals as watch). When a noisy neighbour or a quota explains the spike, it holds and points you at tickwarden host/iostorm: shrinking the world wouldn't give stolen CPU back. Threshold: -starve-psi / starve_psi (PSI some-avg10 %, default 10; 0 disables).
  • GC stalls — the companion's /jvm endpoint (≥0.6.0) exposes heap and GC counters; when the collector ate the poll window (a 300ms G1 pause is a 6-tick freeze), it holds and points you at heap/GC flags via tickwarden tune. Threshold: -gc-stall-pct / gc_stall_pct (% of wall clock, default 15; 0 disables).

The daemon's web control plane (-ui)

tickwarden daemon -ui 127.0.0.1:9226 serves a self-contained dashboard (no CDNs, works air-gapped): live TPS/MSPT, current distances, both cause gates with their reasons, heap occupancy, the decision history, and live knob editing (target MSPT, sim bounds, gate thresholds, the dry-run/apply toggle) — changes take effect on the next decision, no restart. Knob changes are runtime-only; tickwarden.toml stays the durable source. Off by default; a non-loopback bind is refused unless -ui-token is set, because the config endpoint changes a live server.

Dry-run by default:

tickwarden adaptive                 # log decisions only (safe to watch)
tickwarden adaptive -apply          # actually scale distance live

Needs the companion mod (≥0.4.0) for the /setdistance runtime hook.

License

MIT © Backspin Labs. See LICENSE.

About

Hardware-aware, host-aware tuning and bottleneck detection for self-hosted Minecraft servers

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages