Chunk Inspector
Finds out why chunks stay loaded on a heavily modded Minecraft 1.21.1 / NeoForge server.
Every loaded chunk on a vanilla-or-modded server is loaded because some chunk ticket holds it, or because it is within propagation range of one. Chunk Inspector reads the live ticket table, replays the game's own level propagation, and writes a report naming the tickets, the chunks they keep alive, and — if you ask for it — the exact line of code that created each one.
Reports land in <levelDir>/chunk-summaries (i.e. next to level.dat in the world folder).
Two cost tiers
| Always on | -Dchunkinspector.deep=true |
|
|---|---|---|
| Bytecode injected into Minecraft | none | DistanceManager.addTicket, Ticket |
| Per-ticket cost | zero | one stack walk |
| Names the ticket type, level, age, owner key | yes | yes |
| Names the code that created the ticket | no | yes |
The default tier is genuinely free: an access transformer widens a few ticket fields, and a snapshot
walks them on demand. Nothing runs between snapshots. The instrumenting Mixins are not merely
disabled without deep — a Mixin config plugin declines to apply them, so Minecraft's classes are
left untouched.
Reach for deep when the always-on tier shows you which tickets leak but the owner key is opaque.
Many tickets already identify themselves (a NeoForge forced chunk carries the requesting mod's id),
so it is often unnecessary.
Commands
All require permission level 2.
| Command | What it does |
|---|---|
/chunkinspector status |
Cheap in-chat summary of the current dimension |
/chunkinspector why [<chunkX> <chunkZ>] |
Every ticket contributing to one chunk's level, strongest first. Defaults to the chunk you are standing in |
/chunkinspector snapshot |
Writes a report now |
/chunkinspector snapshot full |
... including the expensive chunk-to-ticket attribution pass |
A report is also written every interval ticks and once on shutdown. The shutdown one always runs
the full analysis, because it is the one people come back to.
System properties
Pass these on the server's JVM command line, before -jar / @libraries/....
| Property | Default | Meaning |
|---|---|---|
chunkinspector.enabled |
true |
Master switch. When off, no hooks and no reports |
chunkinspector.deep |
false |
Record the stack trace behind every ticket |
chunkinspector.interval |
600 |
Ticks between automatic snapshots; 0 disables them |
chunkinspector.attribution |
false |
Run the chunk-to-ticket pass on periodic snapshots too |
chunkinspector.retention |
48 |
Snapshot files to keep; 0 keeps everything |
chunkinspector.textReport |
true |
Also write the human-readable latest.txt |
chunkinspector.stackDepth |
24 |
Frames kept per call site (deep mode) |
chunkinspector.maxOrigins |
8192 |
Distinct call sites remembered (deep mode) |
chunkinspector.sampleRate |
1 |
Capture one stack per N ticket adds (deep mode) |
Example — free monitoring, one snapshot per minute, a day's history:
-Dchunkinspector.interval=1200 -Dchunkinspector.retention=1440
Example — a full investigation:
-Dchunkinspector.deep=true -Dchunkinspector.attribution=true
Output
chunk-summaries/
latest.json the newest report
latest.txt the same thing, readable
summary-20260731-103326-periodic.json timestamped history, pruned to `retention`
summary-20260731-103326-command.json
summary-20260731-103340-shutdown.json
latest.txt of a server with a 4x4 /forceload:
══ minecraft:overworld ══ game time 528
chunks: 113 loaded, 61 block-ticking, 25 entity-ticking (1741 tracked)
tickets: 17 on 17 chunks, 17 never expire, 16 from /forceload
by ticket type
forced 16 tickets 16 chunks level>=31 keeps 64 loaded oldest 26s
start 1 tickets 1 chunks level>=30 keeps 49 loaded oldest 26s
most suspicious tickets
chunk 103, 102 forced level 31 chunk 103, 102
-> permanent ticket, alive 26s, keeping 9 chunks loaded; entity-ticking, so mobs and machines run here
loaded chunk clusters
64 chunks centre 101, 101 bounds 98, 98 .. 105, 105 forced x64
49 chunks centre 0, 0 bounds -3, -3 .. 3, 3 start x49
The JSON carries the same information plus everything that did not fit — see
InspectionReport for the field-by-field documentation.
How to read it
- suspects are tickets with no timeout that the game does not manage itself. Only these can
leak forever.
keeping N chunks loadedis a partition, not a sum: each loaded chunk is credited to the single strongest ticket holding it, so the numbers across tickets add up rather than overlap. - loaded chunk clusters group contiguous loaded chunks. A leak usually shows up as one cluster far from spawn with a single cause.
- level is the vanilla chunk level:
<=31entity-ticking,<=32block-ticking,<=33full. An entity-ticking leak is far more expensive than a merely-loaded one. - unattributed lists loaded chunks no ticket explains. Normally empty; entries here mean something is holding chunks outside the ticket system.
Building
./gradlew build # jar + unit tests
./gradlew e2eTest # installs a real NeoForge dedicated server and asserts on its reports
e2eTest is deliberately not part of check: it downloads NeoForge and runs two servers end to end,
taking a few minutes. It boots a dedicated server in each tier, force-loads a 4x4 square of chunks
over the console, and verifies the reports name it.