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.
| Layer | Signals | What they reveal |
|---|---|---|
| Minecraft | TPS, MSPT, Can't keep up | Tick headroom and delay |
| Chunky | Completed chunks, CPS, queue | Throughput and remaining work |
| JVM | Heap use, GC pauses, RSS, thread CPU | Java memory and CPU pressure |
| VM | Per-core CPU, available memory, swap | Host-level resource saturation |
| Disk | Usage, IOPS, latency, queue depth | Region writes and capacity growth |
| Player | Connected users, status latency | Actual 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.
- Measure live heap, GC, RSS, and thread CPU during normal load and peak player count.
- Measure Chunky separately, then scenarios where backup or plugin jobs overlap it.
- Check OS available memory, page cache, and swap.
- Change heap or VM size one variable at a time, not both together.
- 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
No comments yet. Be the first to leave one.
Pending review