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
| class | what it decides | thread |
|---|---|---|
ServerLevel | the order of the tick, and the gates on its own steps — one instance per dimension | Server |
ServerChunkCache | the chunk half of the tick: ticket purging, distance updates, spawning, random ticks, the broadcast | Server |
ChunkMap | which chunks are candidates for spawning, which are entity-ticking, and every player’s view of every entity | Server |
ChunkHolder | one chunk’s pending block and light changes, and which packet shape they become | Server |
LevelTicks | the two scheduled-tick queues, one LevelChunkTicks per chunk, drained by priority | Server |
EntityTickList | which entities are ticked, and a stable view of that set while it is being walked | Server |
PersistentEntitySectionManager | which entities exist and which of them are ticking — the tick list’s only editor | Server; its inbox is filled by IO threads |
TickRateManager | whether this is a normal tick at all, through TickRateManager.runsNormally | Server |
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 step | running | not a debug world | the 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.tickBlockEntities | per 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.
- Purge stale tickets — while running only, so a frozen world holds on to expired portal and pearl tickets indefinitely.
- 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. - 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.
- Update entity tracking —
ChunkMap.tick, ungated, where last tick’s movement becomes packets. - 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 packet | the step that sends it | to whom |
|---|---|---|
ClientboundGameEventPacket, rain and thunder levels | ServerLevel.advanceWeatherCycle | this dimension’s players, and everyone on a start or a stop |
ClientboundGameEventPacket, start and stop raining | the same step, on a transition | every player in every dimension |
ClientboundSetTimePacket | ServerClockManager.moveToTimeMarker, from the sleep skip | everyone, sent by the clock manager |
ClientboundBlockUpdatePacket · ClientboundSectionBlocksUpdatePacket | ChunkHolder.broadcastChanges, per 16³ section | everyone tracking the chunk |
BlockEntity.getUpdatePacket | the same walk, beside a changed position that has one | everyone tracking the chunk |
ClientboundLightUpdatePacket | the same walk, before the blocks | the players at the edge of their tracked area |
| entity moves, and the adds and removes a move causes | ChunkMap.TrackedEntity, inside ChunkMap.tick | whoever tracks that entity |
ClientboundBlockEventPacket | ServerLevel.runBlockEvents | players 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 byServerClockManagerand ticked by the server, and theClientboundSetTimePacketafter a night skip is sent by the clock manager rather than by the level. WhatServerLevelstill owns is gameTime, advanced byServerLevel.tickTimein the overworld alone, which every other dimension reads. The weather countdowns went the same way, into oneWeatherDataon theMinecraftServer.
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.