Skip to content

Chunk Saving

The server continuously saves modified chunks to disk. Chunks are not saved all at once — the save system scans for dirty chunks every 0.5 seconds and flushes them in small batches each tick. For the on-disk format, see Storage Format.

A chunk is saved when any of its components are marked dirty. Each component tracks its own dirty state, and the chunk is considered dirty if any component needs saving:

ComponentTriggers
BlockChunkBlock placed/removed, heightmap changed, tint changed, environment changed, ticking state changed
BlockComponentChunkBlock state modified (instance blocks, teleport config, etc.)
EntityChunkEntity added or removed from chunk
WorldChunkNewly generated chunk (if saveNewChunks config is enabled)

Other systems also mark chunks dirty: fluid simulation, farming ticks, lighting recalculation, entity movement between chunks, and respawn point changes.

Mod-registered ChunkStore components with a codec are serialized alongside built-in components when a chunk saves. Entity components registered on the EntityStore are saved as part of the EntityChunk.

WorldChunk worldChunk = store.getComponent(ref, WorldChunk.getComponentType());
worldChunk.markNeedsSaving();

Or via command: /chunk marksave <x> <z>

The save system runs as a ticking system on each world:

  1. Every 0.5s: scans all chunks for getNeedsSaving() && !isSaving()
  2. Every tick: dequeues up to ForkJoinPool.commonPool().getParallelism() chunks (roughly CPU cores - 1) and saves them asynchronously
  3. On completion: sets ChunkFlag.ON_DISK, clears the dirty flag, marks isSaving = false

A chunk being saved (isSaving = true) is not re-queued until the current save completes.

World config (config.json) is saved separately by WorldConfigSaveSystem, which runs every 10 seconds if the config has changed.

// async - saves all dirty chunks in the world
CompletableFuture<Void> future = ChunkSavingSystems.saveChunksInWorld(store);

This iterates all chunks, queues every dirty one, then drains the entire queue synchronously (not batched). Used internally by the /world save command and on shutdown.

CommandDescription
/world save <world>Save all dirty chunks + world config for a specific world
/world save --allSave all worlds
/chunk marksave <x> <z>Mark a specific chunk as needing save (picked up by next scan)
/world settings chunksaving set <true|false>Toggle chunk saving at runtime

See World Commands and Chunk Commands for details.

Two world config options control saving behavior:

ConfigDefaultDescription
canSaveChunkstrueMaster toggle — when false, no chunks are saved (including on shutdown)
saveNewChunkstrueWhether newly generated chunks are marked dirty. Disable to avoid saving chunks generated by exploration, so worldgen changes apply on next visit
WorldConfig config = world.getWorldConfig();
// disable all chunk saving
config.setCanSaveChunks(false);
// prevent newly generated chunks from being saved
config.setSaveNewChunks(false);

Setting saveNewChunks to false is useful during worldgen development — chunks generated by players exploring won’t be persisted, so regeneration picks up changes on the next visit.

ChunkSaveEvent fires for each chunk about to be saved. It is cancellable — cancelled chunks are skipped.

This is an ECS event dispatched on the ChunkStore, not a regular event bus event. To listen, create an EntityEventSystem:

public class ChunkSaveHandler extends EntityEventSystem<ChunkStore, ChunkSaveEvent> {
private final MyPlugin plugin;
public ChunkSaveHandler(MyPlugin plugin) {
super(ChunkSaveEvent.class);
this.plugin = plugin;
}
@Override
public void handle(int index, ArchetypeChunk<ChunkStore> archetypeChunk,
Store<ChunkStore> store, CommandBuffer<ChunkStore> commandBuffer,
ChunkSaveEvent event) {
WorldChunk chunk = event.getChunk();
// prevent saving a specific chunk
if (shouldSkip(chunk)) {
event.setCancelled(true);
return;
}
// track saved chunks for delta map rendering, etc.
long chunkIndex = ChunkUtil.indexChunk(chunk.getX(), chunk.getZ());
plugin.onChunkSaved(chunkIndex);
}
@Override
public Query<ChunkStore> getQuery() {
return WorldChunk.getComponentType(); // run on all chunk entities
}
}

Register the handler in setup():

this.getChunkStoreRegistry().registerSystem(new ChunkSaveHandler(this));

The event fires continuously as dirty chunks are flushed — not in one large batch. For using this event with map tile invalidation, see Delta Rendering.

Each WorldChunk has lifecycle flags accessible via worldChunk.is(ChunkFlag):

FlagDescription
START_INITChunk initialization has started
INITChunk is fully initialized
NEWLY_GENERATEDChunk was generated (not loaded from disk)
ON_DISKChunk has been saved to disk at least once
TICKINGChunk is currently ticking
// check if a chunk has ever been saved
boolean saved = worldChunk.is(ChunkFlag.ON_DISK);
// check if chunk was loaded from disk vs freshly generated
boolean fromDisk = !worldChunk.is(ChunkFlag.NEWLY_GENERATED);

On world shutdown, the server synchronously saves all remaining dirty chunks before tearing down the world. If canSaveChunks is false, chunks are not saved and a warning is logged.

The server supports automated backups via CLI flags:

FlagDescription
--backupEnable backups
--backup-frequency <minutes>Backup interval (minimum 1 minute)
--backup-dir <path>Backup output directory
--backup-max-count <n>Maximum backups to keep (default 5)

During a backup:

  1. Chunk saving is paused on all worlds
  2. In-flight saves are waited on
  3. The universe directory is zipped to the backup directory
  4. Chunk saving resumes

Old backups beyond maxCount are archived every 12 hours, then deleted.