Skip to main content
Version: Minecraft 26.2

Lifecycle and Threads

This page explains when and on which thread your mod code is called. Each item follows the Javadoc of the type and the actual behavior of the Poly Mod Loader runtime.

At a glance​

JVM start ─ main thread
│
├─ Mod discovery · poly.mod.json check · dependency resolution POLY-METADATA-*, POLY-DEPENDENCY-*
├─ System replacement conflict resolution (before any mod code) POLY-SYSTEM-001/002/004/005
├─ Transform plan finalized POLY-TRANSFORM-*
├─ SystemProvider.create (once per system) POLY-SYSTEM-003
├─ common entrypoint (all mods, dependency order) POLY-ENTRYPOINT-*
├─ client or server entrypoint (dependency order)
├─ Game layer starts POLY-BOOTSTRAP-005
│
└─ Game main
├─ After Minecraft's bootstrap: other loaders' mods initialise (Fabric), then
│ LifecycleEvents.MODS_INITIALIZED
├─ Just before registry freeze: declarations become game objects, registration closes
│ ── later declarations fail with POLY-*-002 ──
├─ Server thread: events, ticks, commands, AI, scheduled tasks, server-side payloads
├─ Client (render) thread: HUD, screens, drawing, client-side payloads
├─ World generation threads: code placement features, density functions, structures, terrain generators
└─ Network thread: payload codecs
│
JVM exit ─ "Poly Mod Loader Shutdown" thread
└─ onShutdown tasks, in reverse registration order POLY-SHUTDOWN-001

Startup order​

StepWhat runsThreadFailure
1Mod discovery, metadata check, dependency resolution, system replacement conflict resolutionmainPOLY-METADATA-*, POLY-DEPENDENCY-*, POLY-SYSTEM-*. No mod code has run yet
2SystemProvider.create of the system being replacedmain, once per systemStartup stops with POLY-SYSTEM-003
3CommonModInitializer of every modmainStartup stops with POLY-ENTRYPOINT-003
4On a client, ClientModInitializer of every mod. On a dedicated server, DedicatedServerModInitializermainSame as above
5Game layer startsmainPOLY-BOOTSTRAP-005
6Game mainmain—
7Inside the game's start, after Minecraft's bootstrap: other loaders' mods initialise where their loader does it (Fabric: main, then client or server), then LifecycleEvents.MODS_INITIALIZEDmainA listener that throws is disabled (POLY-EVENT-001)
  • Steps 3 and 4 each go through all mods in dependency order. A mod that is depended on runs first. Ties are broken by mod ID. So the first client (or server) entrypoint runs only after every common entrypoint has finished.
  • The integrated server in single-player runs inside the client process. In that case the server entrypoint is not called. server runs only on a dedicated server.
  • When an entrypoint runs, the Minecraft registries and game objects do not exist yet. In an entrypoint, only declare.
  • Mods of other loaders (Fabric) initialise later, in step 7, where their own loader runs them. A Poly Mod that needs another loader's mod to be initialised listens to LifecycleEvents.MODS_INITIALIZED. It comes once, after every mod, and at the same point when no other loader's mod is installed. See Compatibility.
context.events().register(LifecycleEvents.MODS_INITIALIZED, () -> {
// Every mod, of every loader, has run its initialisers.
});

Entrypoint kinds and poly.mod.json keys are in Entrypoint.

Game layer and registration close​

The game layer (poly-game-v26_2) starts after all entrypoints finish and before game main. If it cannot start, startup stops with POLY-BOOTSTRAP-005.

Registration closes just before the game freezes the registries.

  1. Poly Mod Loader validates each declaration in an entrypoint on the spot. A wrong or duplicate name throws immediately.
  2. Just before the game freezes the registries, the game layer turns all declarations into real game objects.
  3. Right after that, registration closes. Every later declaration throws IllegalStateException with the message <code> … must be declared in a mod entrypoint; registration is closed.

Always finish declaring inside an entrypoint. Do not declare from an event listener, a scheduled task, or another thread.

Thread per callback​

CallbackThreadWhat to keep in mind
CommonModInitializer, ClientModInitializer, DedicatedServerModInitializermainOnly declare
SystemProvider.createmain, once before the entrypointsNeeds a public no-argument constructor
GameBrandSystem.brandany thread, many timesKeep it stateless and fast
EventRegistry.registerany thread, any timeThread-safe
Server events (ServerEvents, PlayerEvents, BlockEvents, EntityEvents, WorldEvents, InteractionEvents)serverServerEvents.TICK fires every tick, so keep it fast
ClientEventsclientFired only on the client. ITEM_TOOLTIP fires every frame a tooltip is shown, so keep it fast
BlockBehavior, ItemBehaviorthat world's thread, both sidesCheck world.isClientSide() and change state only on the server
BlockEntityTicker, AiGoal, ProjectileBehaviorserverUse a view you receive only during the call
CommandAction, PermissionProviderserverPermission checks are also called often when suggesting commands, so keep them fast
Task from WorldView.scheduleserver, after the given ticks—
ServerPayloadHandler / ClientPayloadHandlerserver / client—
PayloadCodecnetwork threadDo not access game state
ConfigurationExchangeserver part on the server, client reply on the clientSet the server part in the common entrypoint and the client reply in the client entrypoint
HudLayer, BlockEntityPainter, WorldPainterclient (render), every frameKeep it fast. Block entity drawing reads the client copy
All methods of ModScreen, screen buttonsclient (render)—
CodeFeature, StructureBuilderworld generation threads, several chunks at onceKeep state in local variables only. Use only FeatureContext.random() for randomness in placement features
NoiseFunctionworld generation threads, very often and concurrentlyStateless, fast, and always the same value at the same position
TerrainGeneratorworld generation threads, several columns at onceThread-safe, and the same column for the same position and seed
onDataReload, onDataFilesserverAt server start and on /reload
onAssetReload, onAssetFilesclientAt client start and on F3+T. Not called on a dedicated server
Task from onShutdownPoly Mod Loader shutdown thread (Poly Mod Loader Shutdown)See Shutdown below
Views belong to the thread that passed them, and only for the call

Views such as WorldView, EntityView, and PlayerView are valid only on the thread that passed them (server or client), and only while the callback runs. Do not store them in fields or hand them to another thread. In world generation callbacks, use only the context you receive instead of views.

If a callback throws​

CallbackResult
EntrypointPOLY-ENTRYPOINT-003. Startup stops
SystemProvider, system validationPOLY-SYSTEM-003. Startup stops
Event listenerPOLY-EVENT-001. Only that listener is turned off. Other listeners and the game continue. A decision listener that throws or returns null (POLY-EVENT-003) counts as PASS
HudLayer, BlockEntityPainter, WorldPainterPOLY-CLIENT-003. Only that layer or drawing is turned off
CodeFeature, NoiseFunction, StructureBuilderPOLY-WORLDGEN-005. Logged once, and the failed spot is skipped
Task from WorldView.schedulePOLY-SCHEDULE-001. Logged, and execution continues
PayloadCodec.decodePOLY-NETWORK-004. That payload is dropped
Mod code in ConfigurationExchangePOLY-NETWORK-005
BlockBehavior, ItemBehaviorPassed on to the game, like an exception from a game block
Shutdown taskPOLY-SHUTDOWN-001. Logged, and the remaining tasks still run

Registering too late, or on the wrong side​

Declaring after registration has closed (for example inside an event listener) throws IllegalStateException with the area's code.

RegistrationError after close
Blocks, items, entities, projectiles, effects, sounds, particles, game rules, item components, entity data, advancement triggersPOLY-CONTENT-002
CommandsPOLY-COMMAND-002
Payload channels, configuration exchangePOLY-NETWORK-002
Placement features, code placement features, density functions, structures, biomes, terrain generatorsPOLY-WORLDGEN-002
Permission nodes, permission providersPOLY-PERMISSION-002
Reload listenersPOLY-RESOURCE-002
Game testsPOLY-GAMETEST-002
Key bindings, HUD layers, block entity painters, dynamic textures, world painters, shader pipelinesPOLY-CLIENT-002
onShutdown (after shutdown has started)POLY-SHUTDOWN-002

If you pick the wrong side:

SituationResult
Calling mod.client() on a dedicated serverIllegalStateException, POLY-CLIENT-001. Move client registration to the client entrypoint
Sending a payload before connectingIllegalStateException, POLY-NETWORK-002
Calling sendToServer on a dedicated server, or sendToPlayer on a target that is not a server-side playerIllegalStateException
Calling a world-changing method on the clientIllegalStateException
Uploading a dynamic texture from a non-client threadPOLY-CLIENT-005
Declaring content only in the client entrypointThere is no separate error code, but the content list differs from the server and sync breaks. Declare content in the common entrypoint

Shutdown​

mod.onShutdown("close database", () -> database.close());
  • Each task runs once on the Poly Mod Loader shutdown thread (Poly Mod Loader Shutdown) when the JVM exits.
  • Tasks of all mods run in reverse registration order. So a mod that depends on another is cleaned up before the mod it depends on.
  • Keep tasks short, do not start new threads, and do not touch game state. The game state may already be gone.
  • If a task throws, Poly Mod Loader logs it as POLY-SHUTDOWN-001 with the mod ID and keeps running the remaining tasks.
  • Calling onShutdown after shutdown has started fails with POLY-SHUTDOWN-002.

Work to do at the moment the server stops belongs in server events on the server thread, not in a shutdown task: ServerEvents.STOPPING before the world is saved, WorldEvents.UNLOADED for each dimension after it was saved, and ServerEvents.STOPPED after the worlds were closed.

See also​

Buy me a coffee