Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The level tick

Verified against Minecraft 26.3 · Part III · A player stands still for one twentieth of a second while a whole dimension advances — weather, scheduled ticks, spawns, every entity, every block entity.

Nobody moves. A wheat crop is one random tick short of ripe, a creeper is walking a fence line, rain has been falling for four minutes and a piston somewhere is halfway through extending. Then ServerLevel.tick runs, once, for this dimension, and all of it advances: the border interpolates, the weather counts down, the scheduled block ticks fire, mobs are counted and spawned, chunks near players get their random ticks, every entity in range runs its Entity.tick, block entities run their tickers, and the block changes that all of that produced are handed to the clients. It is one method on the Server thread, called once per dimension from MinecraftServer.tickChildren (the server tick), overworld first. The order inside it is the whole lecture, because one of the steps is in a place nobody expects: the block-change broadcast runs before the entities tick. ServerChunkCache.broadcastChangedChunks comes before ChunkMap.tick and long before EntityTickList.forEach, so a block a command changed between two ticks reaches your screen in the tick that follows, and a block a piston changed during that tick reaches it a tick later.

Three ranges, before we need them

Three phrases run through everything below and they are all one number line, which counts outwards: a low level is a chunk somebody is standing in and a high one is a chunk at the edge of what the server bothers with, up to ChunkLevel.MAX_LEVEL. A chunk is loaded when it has a ChunkHolder at all; it is block-ticking at level 32 or below (ChunkLevel.BLOCK_TICKING_LEVEL) and entity-ticking at 31 or below (ChunkLevel.ENTITY_TICKING_LEVEL), and those last two answers come from the simulation graph, through DistanceManager.inBlockTickingRange and DistanceManager.inEntityTickingRange. How a chunk gets its level — which tickets put it there and which graph they feed — is Part IV’s tickets and loading; for this page it is enough that block-ticking reaches one chunk further out than entity-ticking, and that both are brought up to date inside this tick, before anything but the scheduled ticks and the raids reads them.

The cast

classwhat it decidesthread
ServerLevelthe order of the tick, and the gates on its own steps — one instance per dimensionServer
ServerChunkCachethe chunk half of the tick: ticket purging, distance updates, spawning, random ticks, the broadcastServer
ChunkMapwhich chunks are candidates for spawning, which are entity-ticking, and every player’s view of every entityServer
ChunkHolderone chunk’s pending block and light changes, and which packet shape they becomeServer
LevelTicksthe two scheduled-tick queues, one LevelChunkTicks per chunk, drained by priorityServer
EntityTickListwhich entities are ticked, and a stable view of that set while it is being walkedServer
PersistentEntitySectionManagerwhich entities exist and which of them are ticking — the tick list’s only editorServer; its inbox is filled by IO threads
TickRateManagerwhether this is a normal tick at all, through TickRateManager.runsNormallyServer

ServerLevel is one of the two subclasses of the abstract Level, and the split shows in this page’s own names. Level holds what a world has on either side — the block-entity ticker list, the CollectingNeighborUpdater, the two weather floats, Level.random and the level data — and leaves the questions only an authority can answer abstract, Level.sendBlockUpdated and Level.tickRateManager among them. ServerLevel answers those and adds the authority itself: the chunk source, the two scheduled-tick queues, the entity manager, the list of players. So where a step below is spelled with Level rather than ServerLevel the client inherits it too, and the client level is the list of the ones it has hollowed out.

The whole tick, and its three gates

The tick is one method calling its steps one after another, so its shape is not a conversation — it is a column with guards down the side. There are three guards, and every step is behind one of them, a combination of them, or nothing: running (TickRateManager.runsNormally, false while /tick freeze holds and no step is pending), not a debug world (Level.isDebug, true only in the Debug Mode world type), and the dimension is not empty (ServerLevel.emptyTime below ServerLevel.EMPTY_TIME_NO_TICK, 300).

flowchart TD
    START["MinecraftServer.tickChildren calls ServerLevel.tick, overworld first"]
    ENV["ServerLevel.handlingTick goes true, the attribute cache dropped"]
    WB["WorldBorder.tick, then ServerLevel.advanceWeatherCycle"]
    SLEEP["the sleep check: the clock, the players, the weather"]
    SKY["Level.updateSkyBrightness"]
    TIME["ServerLevel.tickTime, overworld only"]
    SCHED["ServerLevel.blockTicks, then ServerLevel.fluidTicks"]
    RAID["Raids.tick"]
    subgraph SCC["ServerChunkCache.tick"]
        direction TB
        PURGE["TicketStorage.purgeStaleTickets"]
        DIST["ServerChunkCache.runDistanceManagerUpdates"]
        CHUNKS["spawns, random ticks, thunder, custom spawners"]
        CAST["ServerChunkCache.broadcastChangedChunks"]
        TRACK["ChunkMap.tick: entity tracking"]
        UNLOAD["ChunkMap.tick with the budget: POI saves and unloads"]
        PURGE --> DIST --> CHUNKS --> CAST --> TRACK --> UNLOAD
    end
    EVENTS["ServerLevel.runBlockEvents, then ServerLevel.handlingTick false"]
    EMPTY["ServerLevel.emptyTime: reset by a ticket, otherwise rising"]
    DRAGON["EnderDragonFight.tick, the End only"]
    ENT["EntityTickList.forEach: each entity, then its riders"]
    BE["Level.tickBlockEntities"]
    EM["PersistentEntitySectionManager.tick: the inbox, then the unload set"]
    DBG["LevelDebugSynchronizers.tick"]
    GC["RandomState.garbageCollect"]
    START --> ENV --> WB --> SLEEP --> SKY --> TIME --> SCHED --> RAID --> PURGE
    UNLOAD --> EVENTS --> EMPTY --> DRAGON --> ENT --> BE --> EM --> DBG --> GC

The order, and the one containment in it: the six steps in the box are not siblings of the steps around them, they are the insides of a single call, and a later section counts them as five things.

The gates are the other half of the tick and they do not fit in the boxes. Read this table down the middle instead: a step with three blank cells runs on a frozen, empty, debug world.

the steprunningnot a debug worldthe dimension is not empty
ServerLevel.handlingTick true, the attribute cache dropped
WorldBorder.tick, ServerLevel.advanceWeatherCycle•
the sleep check
Level.updateSkyBrightness
ServerLevel.tickTime•
ServerLevel.blockTicks, ServerLevel.fluidTicks••
Raids.tick•
TicketStorage.purgeStaleTickets•
ServerChunkCache.runDistanceManagerUpdates
spawns, random ticks, thunder, custom spawners••
ServerChunkCache.broadcastChangedChunks•
ChunkMap.tick, entity tracking
ChunkMap.tick with the budget
ServerLevel.runBlockEvents•
ServerLevel.handlingTick false
ServerLevel.emptyTime rises•
EnderDragonFight.tick••
EntityTickList.forEach•
Level.tickBlockEntitiesper ticker•
PersistentEntitySectionManager.tick
LevelDebugSynchronizers.tick
RandomState.garbageCollect

All but the last of those steps have names, and they are the names a profiler reports: world border and weather, tickPending (holding blockTicks and fluidTicks), raid, chunkSource, blockEvents, entities (holding dragonFight, checkDespawn and the per-entity tick), blockEntities, entityManagement and debugSynchronizers. Half a dozen pages elsewhere in the book place a mechanism by saying it runs after tickPending or in the chunk source phase, and they mean these; so does a /debug profiling report.

Read the gate columns and most of the page’s surprises fall out of the table. Sleeping through the night works with the game frozen. A frozen world still loads, sends and unloads chunks — and stops expiring its tickets. A debug world keeps its entities and drops its block updates. And the last three steps run on a dimension with nobody in it.

What the tick does before anything can move

The cache that is dropped before the border

The first thing the tick does to the world, before the border and before the weather, is EnvironmentAttributeSystem.invalidateTickCache: last tick’s resolved environment attributes are thrown away. That system (environment attributes) is where the old per-dimension and per-biome constants went, and Level.updateSkyBrightness later in this same tick reads EnvironmentAttributes.SKY_LIGHT_LEVEL out of it rather than deriving sky light from the time of day. ServerClockManager invalidates the same cache on every level whenever a clock is set, moved, paused or given a new rate, so the level is not its only owner — it is the first reader of the tick, and it starts clean.

The weather is the server’s; only the fade is the level’s

Then WorldBorder.tick advances the interpolated extent, and ServerLevel.advanceWeatherCycle counts the clear, rain and thunder timers down under GameRules.ADVANCE_WEATHER, resampling the rain and thunder timers from ServerLevel.RAIN_DELAY, ServerLevel.RAIN_DURATION, ServerLevel.THUNDER_DELAY and ServerLevel.THUNDER_DURATION as each expires, and fading Level.rainLevel and Level.thunderLevel by 0.01 a tick, which is why a downpour arrives as a five-second ramp. The countdowns belong to the server: ServerLevel.getWeatherData delegates to MinecraftServer.getWeatherData, one WeatherData shared by every dimension, and the only per-level parts are those two floats and the Level.canHaveWeather test that decides whether this dimension acts on any of it. Every move of a float is a ClientboundGameEventPacket (ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE) to this dimension’s players, and a start or a stop goes, with both levels, to every player in every dimension.

A freeze stops the clock and not the sleep check

SleepStatus.areEnoughSleeping and SleepStatus.areEnoughDeepSleeping (against GameRules.PLAYERS_SLEEPING_PERCENTAGE) decide the night skip, and they are checked outside every gate. If the dimension type has a default clock and GameRules.ADVANCE_TIME is on, ServerClockManager.moveToTimeMarker jumps that clock to ClockTimeMarkers.WAKE_UP_FROM_SLEEP; ServerLevel.wakeUpAllPlayers gets everyone out of bed, and, if it is raining and GameRules.ADVANCE_WEATHER is on, ServerLevel.resetWeatherCycle clears the storm. The ClientboundSetTimePacket that follows is sent by the clock manager, not by the level — day time is not the level’s state at all.

What the level does own is gameTime, and only in the overworld: ServerLevel.tickTime advances it when the level was built with its ServerLevel.tickTime flag set, which MinecraftServer does for the overworld alone, and every other dimension reads that number. The same call advances the server-wide TimerQueue behind /schedule (MinecraftServer.getScheduledEvents), so a scheduled function is timed off overworld gameTime and stands still while the world is frozen.

Scheduled ticks, twice, and a promise to the same block

LevelTicks.tick is called twice — ServerLevel.blockTicks, then ServerLevel.fluidTicks — each with the current gameTime and a budget of ServerLevel.MAX_SCHEDULED_TICKS_PER_TICK, 65536, and both skipped entirely in a debug world. Each call collects every LevelChunkTicks container whose head is due and whose chunk passes ServerLevel.isPositionTickingWithEntitiesLoaded, drains what the budget allows, and hands each drained tick to ServerLevel.tickBlock or ServerLevel.tickFluid — both of which check that what is at the position is still what the tick named before running it. The order the drain uses, the dedup rule that decides what was ever queued, and why that re-check is the whole of cancellation belong to scheduled ticks; what belongs to this page is that the two calls happen here, in this order, under this budget.

The chunk source does five things in one call

ServerChunkCache.tick receives the server’s MinecraftServer.haveTime supplier (or, while sprinting, a constant false in its place) — the level never looks at it, it only passes it on — and does five things in order.

  1. Purge stale tickets — while running only, so a frozen world holds on to expired portal and pearl tickets indefinitely.
  2. Run ServerChunkCache.runDistanceManagerUpdates — ungated, and the tick’s own run of the updates that change a chunk’s ticking state. The reason an entity starts or stops ticking this tick is decided here, several steps before the entity loop reads it.
  3. Tick the chunks, then broadcast — one call holding two halves, both skipped in a debug world: the spawning and random-ticking work below, and then the block changes turned into packets. Each half has a section of its own.
  4. Update entity tracking — ChunkMap.tick, ungated, where last tick’s movement becomes packets.
  5. Spend whatever time is left on POI saving and chunk unloads.

That last step is the only part of the whole level tick that yields to the clock, and even it does not yield completely: ChunkMap.processUnloads force-drains anything over two thousand queued unloads whatever the budget says.

Two chunk sets, and two different mob caps

NaturalSpawner.createState walks ServerLevel.getAllEntities — every entity in the dimension except the ones in MobCategory.MISC (items, projectiles and armour stands, but villagers, golems and minecarts too), and except mobs that require persistence — and counts the rest per MobCategory, using the chunk each one stands in to charge a spawn-potential field and to feed a LocalMobCapCalculator. Walking every entity in the dimension is what this step costs, once a tick, before any spawn is attempted; the two caps that census feeds — a server-wide one scaled by the spawnable-chunk count and a per-player one that is not — belong to entity lifecycle. The categories marked persistent — the animals — are considered only on a tick where gameTime divides by 400, and GameRules.SPAWN_MOBS stops the spawns themselves, not the census before them or the thunder beside them.

Two chunk sets are then walked, and they are not the same set. ChunkMap.collectSpawningChunks gathers the spawning chunks: the chunks within eight of a player (the radius DistanceManager.naturalSpawnChunkCounter counts) that have a ticking chunk and at least one non-spectating player within 128 blocks (ChunkMap.playerIsCloseEnoughForSpawning). They are shuffled, and each then gets ChunkAccess.incrementInhabitedTime, then ServerLevel.tickThunder if it is also entity-ticking, and then NaturalSpawner.spawnForChunk if ServerLevel.canSpawnEntitiesInChunk. The thunder is a 1-in-100000 roll per chunk per tick while raining and thundering, whose bolt prefers a lightning rod, then a mob that can see the sky, then the heightmap, and which brings a trap skeleton horse along at effective difficulty × 1 % when GameRules.SPAWN_MOBS is on and no lightning rod is under the strike.

The second set is ChunkMap.forEachBlockTickingChunk, which despite its name walks DistanceManager.forEachEntityTickingChunk — the entity-ticking set, level 31 and below — and keeps only those whose ChunkHolder has a live ChunkHolder.getTickingChunk. Each survivor gets ServerLevel.tickChunk.

Random ticks are counted per section, and empty sections are free

ServerChunkCache.tickChunks reads GameRules.RANDOM_TICK_SPEED (default 3, minimum 0) once per level tick and hands it down, and ServerLevel.tickChunk does two things with it. It rolls that many 1-in-48 chances at ServerLevel.tickPrecipitation — the ice and snow layer, using Biome.shouldFreeze, Biome.shouldSnow and GameRules.MAX_SNOW_ACCUMULATION_HEIGHT — and then, for every LevelChunkSection in the chunk that reports LevelChunkSection.isRandomlyTicking, picks that many random positions and rolls both BlockBehaviour.BlockStateBase.randomTick and the fluid’s at each. LevelChunkSection.isRandomlyTicking is a counter maintained on every block change, so a section of solid stone costs nothing at all: the loop over sections is a loop over the chunk’s interesting height. Set the rule to zero and both halves stop, ice and snow included.

The position itself does not come from Level.random. Level.getBlockRandomPos advances Level.randValue, a plain linear congruential generator, and unpacks x, y and z out of one integer. Everything the block then does with that position — the crop-growth roll, fire spreading, every implementation of BlockBehaviour.BlockStateBase.randomTick — takes Level.random, which is also what the ice and snow rolls, the lightning roll and the spawning-chunk shuffle use.

The custom spawners come last inside this step. ServerLevel.tickCustomSpawners runs the five a level was constructed with, which in practice means the overworld’s (the other ways in names them). Three of the five carry a game rule of their own (GameRules.SPAWN_PHANTOMS, GameRules.SPAWN_PATROLS, GameRules.SPAWN_WANDERING_TRADERS); the other two carry none, and GameRules.SPAWN_MOBS gates the whole call — though VillageSiege, like the phantom and patrol spawners, also stops when GameRules.SPAWN_MONSTERS is off.

The broadcast, which is why entities are a tick behind

Almost nothing in the game sends a block update at the moment a block changes — a landing FallingBlockEntity is the exception worth knowing, and it is dealt with at the end of this section. ServerLevel.sendBlockUpdated calls ServerChunkCache.blockChanged, which finds the ChunkHolder, records the position in that holder’s per-section set, and — the first time a holder gains a changed section — adds it to ServerChunkCache.chunkHoldersToBroadcast. Then ServerChunkCache.blockChanged returns. The packets are built once, later, in the chunk source’s step.

sequenceDiagram
    participant SL as ServerLevel
    participant SCC as ServerChunkCache
    participant CH as ChunkHolder
    participant Wire as the network

    rect rgba(0, 0, 0, 0.04)
        Note over SL,Wire: before the tick — a command changed a hundred blocks
        SL->>SCC: blockChanged, once per block
        SCC->>CH: blockChanged, one entry in that section's set
        CH-->>SCC: nothing leaves, the holder is only marked
    end
    rect rgba(0, 0, 0, 0.04)
        Note over SL,Wire: this tick, inside ServerChunkCache.tick
        SCC->>CH: broadcastChanges, on every marked holder
        CH->>Wire: ClientboundLightUpdatePacket, if either light filter has anything
        CH->>Wire: one ClientboundBlockUpdatePacket per single-change section
        CH->>Wire: one ClientboundSectionBlocks<br/>UpdatePacket per busier section
        CH->>Wire: BlockEntity.getUpdatePacket beside any changed block entity
        Note over SL,Wire: ChunkMap.tick then turns last tick's movement into packets
    end
    rect rgba(0, 0, 0, 0.04)
        Note over SL,Wire: still this tick, several steps later
        SL->>SL: tickBlockEntities: a piston head finishes its push
        SL->>SCC: blockChanged, marked, and it waits for the next tick
    end

Three bands: the write before the tick, the send inside it, and a write that arrives too late for the send. The dotted arrow in the first band is the hook — a block change leaves nothing on the wire at the moment it happens.

ChunkHolder.broadcastChanges asks two different questions of two different audiences, and it asks the light one first: if either light filter has anything in it, ChunkHolder.PlayerProvider.getPlayers is called with the border-only flag set and one ClientboundLightUpdatePacket goes to the players on the edge of their tracked area, before a single block packet is built. The blocks are then emitted per 16³ section, to everyone tracking the chunk, and the shape depends on how many blocks in that section changed: one ClientboundBlockUpdatePacket for a single change, one ClientboundSectionBlocksUpdatePacket for several, plus BlockEntity.getUpdatePacket for any changed position that carries a block entity. A hundred blocks changed by one command are therefore a handful of packets, one per affected section — not a hundred.

Then ChunkMap.tick runs — chunk tracking for each player, and ChunkMap.TrackedEntity for each entity, which is where ServerEntity.sendChanges turns last tick’s movement into packets. Blocks first, entities second, and the entity loop only after both. The ordering is visible from a client: a player’s /setblock lands in the tick right after it runs, because a player’s command runs as a task between two ticks, before MinecraftServer.tickChildren starts, while a piston head, moved inside a tick after that tick’s broadcast, lands in the tick after the one that moved it. A command typed at the console is as late as the piston, because it too changes blocks after the broadcast: it is drained in the connection phase, which MinecraftServer.tickChildren runs after every level has already broadcast.

Falling sand is the exception that proves the rule, and it is deliberate. FallingBlockEntity calls LevelWriter.setBlockAndUpdate and then, straight after, ChunkMap.sendToTrackingPlayers twice: a ClientboundBlockUpdatePacket of its own, and a ClientboundAddTransientBlockPacket the client hands its renderer as a TransientBlock — so the block the client sees appear is sent in the same tick the entity that placed it vanished, rather than a tick behind it. A handful of other places do the same thing to one player rather than to everyone tracking the chunk. What the client does with all of this is what the client is told.

Block events close the handlingTick window

ServerLevel.runBlockEvents drains ServerLevel.blockEvents — the note-block plays, piston pushes and chest-lid counts queued since the last drain, and any the drain itself raises — running every event inside the block-ticking range and putting the rest back for the next one, and this is the only point in the tick at which it happens. What the queue promises the blocks in it, and why a piston told about a change by a packet handler still moves in the same tick this phase belongs to, is pistons and block events.

ServerLevel.handlingTick went true at the very top of the tick and goes false here, so the whole entity half runs outside that window. Its one reader in the game is PistonBaseBlock, which uses it to tell a piston update raised inside the tick from one raised outside it.

An empty dimension skips exactly three things

ServerChunkCache.hasActiveTickets — really TicketStorage.shouldKeepDimensionActive, which is the players’ simulation tickets and the forced, portal and ender-pearl tickets — resets ServerLevel.emptyTime at this point in the tick, and two other calls reset it: Entity.teleportCrossDimension for the level a non-player entity arrives in, and ServerPlayer.registerAndUpdateEnderPearlTicket, from a thrown pearl’s own tick in the entity loop, for the pearl’s level. Otherwise the counter rises, and it rises only while running, so a frozen dimension never falls asleep. Past ServerLevel.EMPTY_TIME_NO_TICK, 300 ticks, the level skips the dragon fight, the entity loop and the block entities. That is the entire skip: the weather, the scheduled ticks, the chunk source, the block events, the entity manager’s load and unload drain and the debug feed all keep running on a dimension nobody has visited for fifteen seconds.

Every entity, and then its riders

EntityTickList.forEach walks the tick list. An entity is skipped if it has been removed, or if TickRateManager.isEntityFrozen says so — frozen, and neither a player nor something carrying one. Otherwise it gets Entity.checkDespawn, and then ticks only if it is a ServerPlayer or its chunk answers DistanceManager.inEntityTickingRange. A passenger whose vehicle is alive and still lists it is passed over here and ticked by the vehicle instead; a stale link is broken with Entity.stopRiding. ServerLevel.tickNonPassenger calls Entity.commonTick, which counts Entity.invulnerableTime down, records the old position and bumps Entity.tickCount, and then Entity.tick; ServerLevel.tickPassenger runs Entity.rideTick for each rider that is a Player or in the tick list, recursively down the stack. Level.guardEntityTick wraps each one in a crash report titled Ticking entity, so a mob that throws names itself.

The list stays stable under all of that by construction. Membership changes during the loop — a bred baby, a fired arrow, a bolt a trident calls down — arrive through ServerLevel.EntityCallbacks.onTickingStart immediately. EntityTickList allows exactly one EntityTickList.forEach at a time and, on the first add or remove while it is iterating, copies into its EntityTickList.passive map and swaps that with EntityTickList.active: the running loop keeps walking the view it started with, and the new entity waits for the next tick. Which entities are in the list at all is PersistentEntitySectionManager’s answer, through Visibility.fromFullChunkStatus — only FullChunkStatus.ENTITY_TICKING maps to a ticking visibility — with one override, Player.isAlwaysTicking, which keeps a player in the list whatever its chunk is doing; the ServerPlayer test inside the loop is the other half of that exemption, letting the player past the range check. Entities are Part VI’s subject: entity lifecycle.

Block entities reach one chunk further than mobs

Level.tickBlockEntities walks Level.blockEntityTickers, dropping removed tickers as it goes and running the rest — but only those whose position passes ServerLevel.shouldTickBlocksAt, which is the block-ticking range, and only while TickRateManager.runsNormally. That single condition is why a furnace keeps smelting one chunk further out than a zombie keeps walking. A block entity created during the walk — a sculk sensor a catalyst’s spread has just grown — lands on Level.pendingBlockEntityTickers instead, because Level.tickingBlockEntities is true, and is merged in when the next tick’s walk begins. Block entities has the rest.

After the entities: the manager’s drain and the debug feed

PersistentEntitySectionManager.tick drains PersistentEntitySectionManager.loadingInbox, a concurrent queue that chunk storage fills on the Server thread once an IO read returns — entities from freshly loaded chunks join the world here — and then processes PersistentEntitySectionManager.chunksToUnload. The ServerLevel.EntityCallbacks it fires (ServerLevel.EntityCallbacks.onTickingStart, ServerLevel.EntityCallbacks.onTickingEnd) are exactly what add and remove entries in the tick list the previous step walked.

Then LevelDebugSynchronizers.tick pushes this tick’s changed brains, paths and raids to any client subscribed through DebugSubscriptions — and, just before it, arms or clears the neighbour listener on CollectingNeighborUpdater, which sends each neighbour update the moment it happens, depending on whether anyone is subscribed to DebugSubscriptions.NEIGHBOR_UPDATES at all. It is the one step of the level tick whose entire output is diagnostic, and it is outside every gate. The last step is RandomState.garbageCollect, which drops the density buffers world generation pooled and no longer uses.

What leaves the level, and when

the packetthe step that sends itto whom
ClientboundGameEventPacket, rain and thunder levelsServerLevel.advanceWeatherCyclethis dimension’s players, and everyone on a start or a stop
ClientboundGameEventPacket, start and stop rainingthe same step, on a transitionevery player in every dimension
ClientboundSetTimePacketServerClockManager.moveToTimeMarker, from the sleep skipeveryone, sent by the clock manager
ClientboundBlockUpdatePacket · ClientboundSectionBlocksUpdatePacketChunkHolder.broadcastChanges, per 16³ sectioneveryone tracking the chunk
BlockEntity.getUpdatePacketthe same walk, beside a changed position that has oneeveryone tracking the chunk
ClientboundLightUpdatePacketthe same walk, before the blocksthe players at the edge of their tracked area
entity moves, and the adds and removes a move causesChunkMap.TrackedEntity, inside ChunkMap.tickwhoever tracks that entity
ClientboundBlockEventPacketServerLevel.runBlockEventsplayers within 64 blocks

None of them go out when they are written. Every one is queued behind the suspended flush that MinecraftServer.tickChildren opens around all the levels, and leaves on the wire when Connection.tick flushes the channel in the connection phase, after every level has ticked (the server tick).

Questions players ask

Does /tick freeze stop the server doing work? Barely, on the chunk side. Distance updates, the block-change broadcast, entity tracking and chunk unloads all run every tick regardless. Two things inside the chunk source are frozen and are easy to miss: the spawning and random-ticking step, and TicketStorage.purgeStaleTickets — so a frozen world accumulates expired tickets and never releases the chunks they hold.

Why did replacing a block cancel its scheduled tick? Because a scheduled tick is a promise to that block: ServerLevel.tickBlock compares the Block at the position with the one scheduled, and a mismatch runs nothing.

Why does /weather rain in the Nether change the overworld? There is one WeatherData on the MinecraftServer, and the command writes it from whatever dimension it runs in; only a dimension that Level.canHaveWeather counts it down. The fade of Level.rainLevel and Level.thunderLevel is per level, and only there.

Why does an arrow a skeleton fires not move until the next tick? EntityTickList swapped its maps the moment the arrow was added, so the loop that is running kept the view it started with. The arrow is in the list — it is just not in this walk of it. A mob that spawned naturally is ticked at once: the chunk source added it before the walk began.

For a 1.21-era reader. The day–night cycle is out of the level. Time is a set of WorldClocks owned by ServerClockManager and ticked by the server, and the ClientboundSetTimePacket after a night skip is sent by the clock manager rather than by the level. What ServerLevel still owns is gameTime, advanced by ServerLevel.tickTime in the overworld alone, which every other dimension reads. The weather countdowns went the same way, into one WeatherData on the MinecraftServer.

Where to look

In the order the tick runs them. ServerLevel.tick · EnvironmentAttributeSystem.invalidateTickCache · ServerLevel.advanceWeatherCycle · SleepStatus.areEnoughSleeping · ServerClockManager.moveToTimeMarker · ServerLevel.tickTime · LevelTicks.tick · ServerChunkCache.tick · ServerChunkCache.tickChunks · NaturalSpawner.createState · ServerLevel.tickChunk · ServerChunkCache.broadcastChangedChunks · ChunkHolder.broadcastChanges · ChunkMap.tick · ServerLevel.runBlockEvents · EntityTickList.forEach · Level.tickBlockEntities · PersistentEntitySectionManager.tick · LevelDebugSynchronizers.tick · RandomState.garbageCollect


Rules: names, never code · how the system works, not how the code reads · newest version only · every backticked name passes tools/verify_names.py.