Optimizing the server's hot paths

How I find and cut overhead from the loops that run on every block break, kill, and tick on the MCHub network.

On this page 5 sections
  1. Find the lag before guessing at it
  2. Stop doing avoidable work
  3. Batch the writes
  4. Math in tight loops
  5. Threading

On a busy prison or skyblock server, a handful of code paths run constantly: block-break, mob-kill, sell, and per-tick entity work. A few microseconds of wasted work in one of those loops is invisible on its own, but once hundreds of players are mining at the same time it compounds into whole milliseconds per tick, and that is what players feel as lag. Most of the work here is about measuring where the time actually goes, then not doing avoidable work in the places that run the most.

Find the lag before guessing at it

Optimization starts with measurement, not hunches. The network runs LagFinder (written by Abeoji), a per-world tick watchdog that samples tick timings and, when a world blows past a threshold, auto-attributes the spike to the system responsible and writes it to a dated, rotated log that turns itself off after an hour so it never fills the disk. I lean on it alongside spark profiling, which together turn “the server feels laggy” into “this specific listener is eating eight milliseconds a tick,” the difference between fixing the real bottleneck and optimizing something that never mattered.

Stop doing avoidable work

Once a hot path is identified, the wins are mostly about not repeating work and not allocating:

  • Cache closure lookups. Resolving an Exports.ptr(...) pointer is a map lookup. Doing it once above a loop instead of every iteration removes thousands of lookups a tick.
  • Avoid allocations. Using x >> 4 for block-to-chunk coordinates instead of block.chunk (which allocates a Chunk object), reusing arrays, and boxing integers once instead of per call.
  • Kill dynamic dispatch. Marking the hottest per-break accessors @CompileStatic so Groovy compiles direct calls instead of dynamic ones, and comparing Materials by enum identity instead of going through reflection.
  • Cut redundant work. Not deserialising treasure data on every kill just to validate it, dropping a per-voxel hash from mine scans, and detecting held-item changes by stack identity instead of deep-hashing the item every few ticks.

Batch the writes

The other big lever is write volume. Systems like the battle pass and mastery earn progress through many tiny calls fired off block-break and kill events, and if each one writes straight to the database that load piles up fast. Instead, progress accumulates in a thread-safe per-player buffer and flushes on a short interval or at a meaningful boundary (logout, level-up, the end of a sell cycle). One periodic flush of many batched updates beats many individual writes.

Math in tight loops

Per-rank and per-block math runs millions of times, so the small stuff matters: integer division with intdiv instead of Groovy’s BigDecimal divide, BigDecimal.valueOf over new BigDecimal(double), hoisting invariant multiplier lookups above the loop, caching values that only change at bucket boundaries (prestige every 1000 ranks, ascension every 5000), and branching out the common multiplier == 1.0 case so it skips the BigDecimal multiply entirely.

Threading

The server runs regionised, multi-threaded world threading, so world and entity work goes through world.execute { } to run on the owning region’s thread rather than Schedulers.sync(), which would funnel everything through a single global main thread and recreate the bottleneck. Shared state that more than one thread touches uses ConcurrentHashMap and atomic merge() increments instead of read-modify-write races.