Chunk Pregeneration and VM Capacity Planning

I pregenerated 1.56 million chunks with Chunky and recalculated VM capacity from TPS, MSPT, CPU, JVM memory, and disk usage.

Generating new terrain involves noise calculation, structure placement, lighting, and region-file writes. When that work happens during a player’s first visit, it competes with tick processing for CPU and disk. Pregenerating the expected exploration area with Chunky moved that cost outside the main play session.

The operational record shows a 10,000-block radius, 1,565,001 completed chunks, and a runtime of 15 hours, 53 minutes, and 18 seconds—an overall average of 27.36 chunks per second. The same log window contains 649 Can't keep up warnings and three player connections. Those observations overlap in time; they do not prove that Chunky caused every warning.

Radius, Selection Shape, and Workload

The observed 1,565,001 chunks equal 1,251². A 10,000-block radius is 625 chunks; including the center gives a square 1,251 chunks wide. The original command is missing, but the exact count strongly suggests a square selection rather than a circle. A circle with the same radius would be approximately π × 625², or 1,227,185 chunks.

flowchart LR
  accTitle: Effects of pregeneration radius and shape
  accDescr: Radius and shape determine the selected area and chunk count, which in turn affect runtime, world size, backup duration, and restore time.
  R[Radius and shape] --> A[Selected area]
  A --> C[Target chunks]
  C --> CPU[CPU and tick load]
  C --> IO[Region-file writes]
  C --> TIME[Runtime]
  IO --> SIZE[World size]
  SIZE --> BACKUP[Backup and upload time]
  SIZE --> RESTORE[Restore time/RTO]

For either a circle or a square, doubling the radius roughly quadruples the area. At the same radius, a square covers about 4/π, or 27% more area than a circle. Even with constant throughput, runtime approaches four times as long; in practice, region-file growth, page cache, GC, and disk behavior can make scaling nonlinear. Choose the radius only after defining the expected exploration boundary, shape, and acceptable maintenance window.

The chunky command is executed after fixing the world, shape, center, and radius.

/chunky world world
/chunky shape square
/chunky center 0 0
/chunky radius 10000
/chunky start

The actual center and world should follow the server’s exploration policy. The Nether is a separate world with a different coordinate scale, so the same radius should not be applied mechanically. Completing the Overworld does not mean every dimension has been pregenerated.

Playability Signals Beyond CPS

Fifteen hours, 53 minutes, and 18 seconds is 57,198 seconds. 1,565,001 ÷ 57,198 gives an overall average of 27.36 CPS. That average helps compare workload scale, but hides stalls, pauses, GC, and periods when players were connected.

The following signals should be collected on the same timeline.

LayerSignalsWhat they reveal
MinecraftTPS, MSPT, Can't keep upTick headroom and delay
ChunkyCompleted chunks, CPS, queueThroughput and remaining work
JVMHeap use, GC pauses, RSS, thread CPUJava memory and CPU pressure
VMPer-core CPU, available memory, swapHost-level resource saturation
DiskUsage, IOPS, latency, queue depthRegion writes and capacity growth
PlayerConnected users, status latencyActual gameplay impact

TPS is the number of ticks completed per second, usually targeting 20. As MSPT approaches 50 ms, the server has little headroom to sustain 20 TPS. Acceptable average TPS can still hide long-tail MSPT or GC pauses that players experience as stutter.

Much of Minecraft’s critical work is concentrated on one thread. A four-vCPU VM can report 35% total CPU while one core is saturated by the tick thread. Looking only at average CPU may lead to adding cores that do not relieve the bottleneck. Per-thread CPU, MSPT, and disk latency need to be read together.

pid=$(systemctl show --property MainPID --value minecraft.service)
pidstat -p "$pid" -t 10
vmstat 10
iostat -xz 10
df -h /opt/minecraft /backup

Record whether each measurement tool is installed and what overhead it adds. For a long run, do not judge performance from a terminal snapshot; save timestamped output or monitoring time series.

A Small-Area Baseline and Stop Conditions

Starting with the full radius can leave a bad limit running for 16 hours. Establish a small-area baseline, measure load and recovery, and only then expand the range.

timeline
  accTitle: Chunk pre-generated load test time base
  accDescr: Establish a baseline, generate a small radius, introduce player load, pause at a threshold, and measure recovery before choosing the next limit.
  title Pregeneration measurement sequence
  Baseline : Normal TPS and MSPT : heap and RSS : per-core CPU : disk latency
  Warm-up : Start Chunky on a small radius : wait for CPS to stabilize
  Load : Connect test players : move and place blocks
  Pause : Pause Chunky at a threshold
  Recovery : Measure return of MSPT and latency to baseline
  Tune : Lower the rate or reduce the work unit

The stop condition comes from game-server headroom, not maximum CPS. Examples include:

  • A sustained rise in MSPT and fall in TPS
  • Longer GC pauses or growing old-generation occupancy
  • Low available memory or swap activity
  • Rising disk latency or queue depth, or loss of backup headroom
  • Slow status responses or failed actions for connected players

Do not copy fixed thresholds from another server. Plugins, Java and Minecraft versions, world generator, disk type, and concurrent players all change the baseline. If resources do not recover after pausing Chunky, the problem may be heap pressure, GC, a queued backlog, or disk saturation—not just generation speed.

The minimum runbook for operators to use is simple.

/chunky pause
/chunky continue
/chunky progress
/chunky cancel

Automation should follow repeated tests that establish stable signals and thresholds. Reacting to single alerts can thrash between pause and resume, so the controller needs duration, cooldown, and a manual override.

Heap, RSS, and VM Memory

The remaining systemd units included -Xms8G, -Xmx8G, and AlwaysPreTouch.

[Service]
WorkingDirectory=/opt/minecraft
ExecStart=/usr/bin/java \
  -Xms8G -Xmx8G \
  -XX:+AlwaysPreTouch\
  -jar purpur.jar nogui
Restart=always
RestartSec=10

Terraform’s default machine type and the README specification differed, so the repository alone cannot prove which VM ran with these JVM flags. Xmx=8G does not mean an 8 GB VM is safe.

In addition to the heap, JVM process memory uses the following:

  • metaspace and compressed class space
  • thread stack
  • JIT code cache
  • direct/native buffer and JNI
  • GC data structure
  • memory-mapped files

The VM also needs memory for the Linux kernel, systemd, Google guest agent, Discord bot, backup tools, and filesystem page cache. Xms=Xmx with AlwaysPreTouch commits and touches the heap early. That can reduce runtime variation, but consumes memory and startup time immediately after boot. Its effect on Minecraft readiness after Spot recovery should be measured too.

flowchart TD
  accTitle: Determine Minecraft VM memory budget and machine type
  accDescr: Start with the measured live heap, add GC headroom, JVM native memory, the OS, page cache, and concurrent bot and backup work, then check CPU and disk separately.
  LIVE[Live heap under load] --> GC[Xmx with GC headroom]
  GC --> JVM[JVM native memory, threads, and buffers]
  JVM --> OS[OS and guest agent]
  OS --> CACHE[filesystem page cache]
  CACHE --> SIDE[Discord bot and backup]
  SIDE --> BURST[Chunky and recovery overlap]
  BURST --> VM[Required VM memory]
  VM --> CHECK{Enough CPU and disk?}
  CHECK -- No --> BOTTLENECK[Adjust the bottlenecked layer]
  CHECK -- Yes --> LOAD[Retest the same scenario]

If Native Memory Tracking is available, isolate off-heap usage.

jcmd "$pid" GC.heap_info
jcmd "$pid" VM.native_memory summary
ps -o pid,rss,vsz,etime,cmd -p "$pid"
cat /proc/meminfo | sed -n '1,20p'

NMT may require JVM startup flags and adds measurement overhead, so validate it in a test environment before production. RSS also does not mean every page is uniquely owned. Memory sizing should use time series and known concurrency scenarios, not one snapshot.

Comparing N2 Machine Types

The N2 standard series provides 4 GB of memory per vCPU. In the current specification, n2-standard-2 has 2 vCPUs and 8 GB, while n2-standard-4 has 4 vCPUs and 16 GB. An 8 GB VM is already unsuitable for an 8 GB heap because it leaves no room for native memory. A 16 GB VM is not automatically sufficient either.

variable "machine_type" {
  type = string
  description = "Compute Engine machine type tailored to measured workload"
  default="n2-standard-4"
}

resource "google_compute_instance" "minecraft" {
  name = "minecraft-server"
  zone = var.zone
  machine_type = var.machine_type
}

Machine type selection is repeated in the following order.

  1. Measure live heap, GC, RSS, and thread CPU during normal load and peak player count.
  2. Measure Chunky separately, then scenarios where backup or plugin jobs overlap it.
  3. Check OS available memory, page cache, and swap.
  4. Change heap or VM size one variable at a time, not both together.
  5. Measure again with the same world, version, radius, and user scenario.

Doubling vCPU count does not make the main tick thread twice as fast. Additional cores can absorb Chunky workers, GC, networking, and side processes, reducing interference with the tick thread. Confirm the real gain through per-core CPU and MSPT.

Including Disk and Backups in Capacity

Terrain generation is both CPU work and permanent data growth. As the world expands, so do the daily working copy, compressed archive, upload traffic, and restore duration. When the local backup path shares the same Persistent Disk, the live world, working copy, and archive may coexist.

The free space required is not simply one times the current world size.

peak local bytes
  = live world
  + working copy
  + compressed archive
  + growth during operation
  + filesystem safety margin

Compression ratio depends on region contents and plugin files. Measure world and archive size before and after a small test area, then estimate the full radius. Keep already-generated areas, the Nether and End, logs, and plugin data separate in that estimate.

Separating Observed Results from Estimates

flowchart LR
  accTitle: Flow from observation to next capacity test
  accDescr: Preserve completion records separately from warnings observed in the same window, test a bottleneck hypothesis with additional metrics, and change one capacity variable at a time.
  OBS[Observed: 1,565,001 chunks in 15:53:18] --> CPS[Average: 27.36 CPS]
  LOG[Same window: 649 warnings and 3 joins] --> CORR[Correlation only]
  CPS --> NEXT[Next small test]
  CORR --> NEXT
  NEXT --> METRIC[TPS, MSPT, thread CPU, GC, disk]
  METRIC --> CAUSE[Test bottleneck hypothesis]
  CAUSE --> SIZE[Change heap, VM, or rate one at a time]

The surviving record proves the completed chunk count, total duration, average CPS, and the number of warnings and joins in the same time window. It does not contain MSPT, GC, CPU, or disk-latency time series around each warning. The 649 warnings therefore cannot all be attributed to Chunky, nor can they prove that a particular VM size was appropriate.

Future run records should include a non-secret world label—not the seed—plus server, Java, and plugin versions, machine type, JVM flags, disk type, selection, start and finish time, pause count, resource time series, world-size change, and backup duration. What makes 27.36 CPS reusable is not the number alone but the measurement context needed to reproduce it.

References

Share

Share

Image preview