The server tick
Verified against Minecraft 26.3 · Part III · One 50 ms tick on the Server thread, from the moment the clock says now to the moment the thread parks again.
Twenty times a second the Server thread wakes, hands every packet that
arrived since it last looked to the code that answers it, advances every
dimension by one step, pushes what each player needs to know onto the wire,
and then spends whatever is left of its 50 ms on work other threads posted
back before parking until the next beat. Everything a player calls the
world — blocks, mobs, weather, the contents of a chest — is computed in
plenty of places and committed in this one, on this one thread. When a
lap runs long the console eventually prints Can’t keep up! Is the server
overloaded?, and that line is not a warning that the server is about to
start missing ticks. MinecraftServer.runServer logs it and advances the
deadline past the whole backlog in the same branch, so the message is
the skip; and because that branch will not fire again for a further ten
seconds and a hundred ticks of the server’s own scheduled time, a server that
complained recently keeps running behind rather than skipping at all. And the
ticks it does skip are simply gone — nothing runs them later, so game time
ends up that many ticks younger than the wall clock and stays there.
The cast
| class | what it decides | thread |
|---|---|---|
MinecraftServer | the loop, the thread and the event loop in one object: the deadline, the budget, and the order everything happens in | Server |
ServerTickRateManager | how long this tick is, whether game elements advance at all, and whether the loop is sprinting | Server |
TickTask | one queued runnable plus the MinecraftServer.tickCount it was submitted on — the age the budget reads | any thread submits, Server runs |
PacketProcessor | which serverbound packets are waiting, and that all of them are handled at the top of a tick | Netty fills it, Server empties it |
ServerLevel | one dimension’s step — a lecture of its own | Server |
ServerConnectionListener | which connections are still alive, and when each one’s player takes its own tick | Server |
ServerCommonPacketListenerImpl | whether a packet handed to a player is written, or written and flushed | Server, mostly |
ServerChunkCache.MainThreadExecutor | what the leftover milliseconds buy, one queue per level | Server |
One lap, ending on the wire
sequenceDiagram
participant PP as PacketProcessor
participant MS as MinecraftServer
participant SL as ServerLevel
participant SGPL as ServerGamePacket<br/>ListenerImpl
participant Conn as Connection
participant Wire as the network
Note over MS,SGPL: MinecraftServer.runServer moves MinecraftServer.nextTickTimeNanos past the backlog if it warns
MS->>PP: processQueuedPackets
PP->>SGPL: each packet Netty queued since the last drain, handled on its listener
rect rgba(0, 0, 0, 0.04)
Note over PP,Wire: MinecraftServer.tickChildren, one pass
MS->>SGPL: suspendFlushing, on every player's listener
MS->>SL: tick(haveTime), each dimension in turn, overworld first
SL->>SGPL: send, per player
SGPL->>Conn: send, with no flush while the flag holds
MS->>Conn: Connection.tick on each, from ServerConnectionListener.tick
Conn->>Conn: flushQueue drains pendingActions
Conn->>SGPL: tick, the player's own tick
Conn->>Wire: flush one, the levels and the player tick
MS->>SGPL: the next chunk batch, through its PlayerChunkSender
MS->>SGPL: resumeFlushing, per player, right after that batch
SGPL->>Conn: flushChannel
Conn->>Wire: flush two, the chunks and the rest of the phase
end
Note over MS,SGPL: MinecraftServer.waitUntilNextTick runs all tasks, then BlockableEventLoop.managedBlock parks
One tick, and the two moments the tick’s own packets reach the socket; the
shaded band is MinecraftServer.tickChildren, the stretch in which a playing
client’s packets wait to be flushed.
Follow the two arrows into the network: they carry what the tick itself sends a playing client, and the second one is the one the chunks ride.
The deadline moves before the work starts
MinecraftServer.runServer re-reads this tick’s length every iteration from
TickRateManager.nanosecondsPerTick and adds it to
MinecraftServer.nextTickTimeNanos before calling anything. Being behind
means the wall clock has already passed that field. The overload branch fires
when it has passed by more than MinecraftServer.OVERLOADED_THRESHOLD_NANOS
(one second) plus MinecraftServer.OVERLOADED_TICKS_THRESHOLD ticks’ worth —
two seconds in all at the default rate — and the last warning is at least
MinecraftServer.OVERLOADED_WARNING_INTERVAL_NANOS (ten seconds) plus
MinecraftServer.OVERLOADED_TICKS_WARNING_INTERVAL ticks’ worth behind it,
fifteen seconds in all at the default rate. Both effects — the log line, and
MinecraftServer.nextTickTimeNanos jumping over the missed ticks — are
statements of that one branch. Lateness therefore has to accumulate before
either happens: a server running every lap ten percent long says nothing while
the shortfall piles up, then warns and drops the pile in one step. And because
MinecraftServer.lastOverloadWarningNanos is set to the new deadline, the
second gate is fifteen seconds of the server’s own scheduled time — which on
an overloaded server is rather more than fifteen seconds of yours. In between,
the backlog is real and every tick of it is run.
Sprinting takes the other arm of the loop’s opening if, the one the overload
check sits inside: when the server is not paused,
ServerTickRateManager.isSprinting is true and
ServerTickRateManager.checkShouldSprintThisTick consents, this tick is
declared zero nanoseconds long and MinecraftServer.nextTickTimeNanos is
set to now. TickRateManager.nanosecondsPerTick still reads 50 ms
throughout — a sprint changes the length of this tick, gives the overload
check nothing to measure, and sends the loop straight back for another. Sprinting also unfreezes the game —
ServerTickRateManager.requestGameToSprint remembers the old state in
ServerTickRateManager.previousIsFrozen — and restores it when
ServerTickRateManager.finishTickSprint reports the measured rate.
Every packet since last time, in one drain
MinecraftServer.processPacketsAndTick opens the frame for the Tracy profiler, then calls
PacketProcessor.processQueuedPackets — before MinecraftServer.tickServer,
so the frame a profiler shows includes the packets. Each entry in that
ConcurrentLinkedQueue is a PacketProcessor.ListenerAndPacket: a Netty
thread decoded the packet, the handler called
PacketUtils.ensureRunningOnSameThread, and that queued the pair here and
aborted the Netty-side call by throwing RunningOnDifferentThreadException
(the connection
has the crossing, in both directions). This is where most
player input enters the world, but not all of it: the handlers that never call PacketUtils.ensureRunningOnSameThread reach the Server thread another way, when they reach it at all.
Fifty-one of ServerGamePacketListenerImpl’s sixty-one game handlers open
on that call, and the ten that do not divide into four kinds
(the handlers that never hop
lists them). Two really touch nothing — the ping reply and an empty
custom-payload hook. Four — ServerGamePacketListenerImpl.handleChat, both
command packets and the command-suggestion request — run their work through
MinecraftServer.execute (the suggestions by way of a
ServerCommandSuggestionsProvider, which keeps only the latest request,
answers at most one a tick, and leaves one it has no budget for to the
player’s own tick), and two more, filtered sign and book text, come
back on a CompletableFuture completed against the server: so chat and
commands arrive as tasks, drained by the event loop below, and not with the
packets. The last two write listener state on Netty
outright, because there is nothing to hand it to — a chat acknowledgement
advances LastSeenMessagesValidator under a lock, and the acknowledgement that
ends the play phase installs the configuration listener. And the chat family
does a little real work before it hands over:
ServerGamePacketListenerImpl.tryHandleChat reads the sender’s chat
visibility and resets its last-action time on the Netty thread, which is
the one place player state is written off the main thread by design.
Two gates sit on each queued pair. PacketListener.shouldHandleMessage is
asked again at handling time, so a player who disconnected between arrival and
now is dropped with a debug line rather than handled into a dead session. And
a handler that throws does not end the tick: ServerPacketListener overrides
PacketListener.onPacketError to log “suppressing error” and return, and
ServerCommonPacketListenerImpl.onPacketError additionally files the
throwable through MinecraftServer.reportPacketHandlingException into the
SuppressedExceptionCollector that the next crash report dumps. Two things
escape: a ReportedException wrapping an OutOfMemoryError, which
PacketUtils.makeReportedException builds for the processor to throw, and any
Error, which the catch does not cover.
An empty server stops ticking
Before anything else MinecraftServer.tickServer compares
MinecraftServer.emptyTicks against MinecraftServer.pauseWhenEmptySeconds
times twenty — the pause-when-empty-seconds property, default 60 in
DedicatedServerProperties, and zero on the base class, which disables the
feature. The counter advances only while nobody is online and the loop is
not sprinting; on the tick it first reaches the threshold the server logs,
autosaves once, and from then on runs MinecraftServer.tickConnection and
returns before the rest of the tick. Only the dedicated server pauses this
way, and its override still ticks the JSON-RPC ManagementServer after that
return. MinecraftServer.tickCount does not advance, so a paused server
is stopped in every sense that matters and still answers pings.
The integrated server pauses on a different signal.
IntegratedServer.tickServer sets IntegratedServer.paused from
Minecraft.isPaused or an empty player list, saves once on the way in, runs
IntegratedServer.tickPaused — connections plus one statistic — instead of
the real tick, and re-syncs the world time on the way out. It also copies the
client’s render and simulation distance into the PlayerList on every
unpaused tick, which is why singleplayer has no separate view-distance
setting.
That pause is the pattern for every difference between the two servers on
this page. The loop itself is written once: IntegratedServer and
DedicatedServer override pieces of the tick and never the loop, so
DedicatedServer.tickServer adds the JSON-RPC ManagementServer.tick,
DedicatedServer.tickConnection the console, and IntegratedServer.tickServer
the pause above. Starting a
server
is how the thread that runs MinecraftServer.runServer comes to exist.
What MinecraftServer.tickChildren runs, and in what order
MinecraftServer.tickChildren is the tick, and the rows below are its
order. All but the first are a profiler section of their own; suspending the
flush has none, and the debug row is three:
| in order | what it does | skipped when |
|---|---|---|
| suspend flushing | ServerCommonPacketListenerImpl.suspendFlushing on every player’s connection | never |
| command functions | ServerFunctionManager.tick runs ServerFunctionManager.LOAD_FUNCTION_TAG once after a reload, then ServerFunctionManager.TICK_FUNCTION_TAG | frozen |
| clocks | ServerClockManager.tick advances the world clocks, a SavedData kept in the world_clocks file | frozen, or GameRules.ADVANCE_TIME is off |
| time sync | MinecraftServer.forceGameTimeSynchronization broadcasts a ClientboundSetTimePacket | not a multiple of 20 ticks |
| levels | MinecraftServer.updateEffectiveRespawnData, then ServerLevel.tick for each dimension in MinecraftServer.getAllLevels order, overworld first | never |
| connection | MinecraftServer.tickConnection — every Connection, and each playing client’s own tick | never |
| players | PlayerList.tick broadcasts a latency-only ClientboundPlayerInfoUpdatePacket | its own counter has not passed 600, so it fires on every 601st call |
| debug, game tests, tickables | ServerDebugSubscribers.tick, GameTestTicker.tick, the dedicated server GUI’s refresh through MinecraftServer.addTickable | game tests alone, when frozen |
| send chunks | PlayerChunkSender.sendNextChunks, then ServerCommonPacketListenerImpl.resumeFlushing, per player | never |
Three rows say frozen, and that is TickRateManager.runsNormally answering
false. /tick freeze sets TickRateManager.isFrozen, and
TickRateManager.tick turns that into this tick’s
TickRateManager.runGameElements — unless /tick step left
TickRateManager.frozenTicksToRun above zero, which it also decrements.
Everything that consults TickRateManager.runsNormally stops: the functions,
the clocks and the game tests in the table above, and inside a level every step
the level tick marks
running, every entity but a player or what carries one, and a player’s
pushing of entities and the movement packets’ rate check.
A throwable out of ServerLevel.tick is caught, filled with the level’s
details as “Exception ticking world” and rethrown as a ReportedException
— which is how one bad dimension ends the whole server. Nothing else in the
list is wrapped. Two things a reader looks for here are elsewhere: the
/schedule queue is a TimerQueue the server owns and persists
(MinecraftServer.getScheduledEvents) but ticks from inside
ServerLevel.tickTime, which runs in the overworld alone and off the
overworld’s gameTime (the level
tick), and the last
statement of MinecraftServer.tickChildren is ServerActivityMonitor.tick,
a rate-limited nudge to the NotificationManager rather than anything the
world can see.
Where a player’s own tick actually happens
The connection phase runs after every level.
ServerConnectionListener.tick walks its synchronized list and, for each live
Connection, calls Connection.tick: drain Connection.pendingActions, the
one queue a Connection owns and which holds closures rather than packets;
tick the
TickablePacketListener; drop the connection if it has died; flush the
channel; and every twentieth tick recompute the packet-rate averages. For a
playing client that listener is ServerGamePacketListenerImpl, whose
ServerGamePacketListenerImpl.tick acknowledges pending block changes, runs
ServerPlayer.doTick through ServerGamePacketListenerImpl.tickPlayer, and
then, only if that returns without having kicked anyone (or at once, with no
player tick, while the server is paused), four more things in this order: the
fifteen-second keep-alive
(ServerCommonPacketListenerImpl.LATENCY_CHECK_INTERVAL), the three spam
throttles, any command-suggestion request still waiting, and the idle-timeout
check.
So a movement packet is applied to the player before any level ticks, and the
player entity takes its step after all of them. Entities see the player
where the packets put her; the player then ticks against a world that has
already moved. An exception out of Connection.tick ends the tick only on singleplayer’s
in-memory channel; elsewhere what it ends depends on the channel rather than
on the fault (the
connection). On a
dedicated server DedicatedServer.tickConnection adds
DedicatedServer.handleConsoleInputs, which is how a command typed at the
console reaches the Server thread. RCON does not come this way:
DedicatedServer.runCommand puts the command on the task queue with
BlockableEventLoop.executeBlocking and waits for the answer, so it runs
wherever the queue next drains. Players and sessions is
what happens inside that phase; the
connection
is the channel underneath it.
The two writes each client gets
ServerCommonPacketListenerImpl.suspendFlushing at the top of
MinecraftServer.tickChildren sets a flag that turns
ServerCommonPacketListenerImpl.send into a channel write with no flush —
but only for sends made on the Server thread, because the flag is tested
together with BlockableEventLoop.isSameThread, so anything sent from another
thread flushes on its own as before. Two things then empty the buffer.
Connection.tick flushes the channel unconditionally, in the middle of its
own body and so inside the connection phase, carrying everything the levels
and the player’s own tick produced. ServerCommonPacketListenerImpl.resumeFlushing, after the chunk
batch, both clears the flag and calls Connection.flushChannel itself,
carrying the player-list update, the debug subscribers, the game-test ticker,
the server’s own tickables and last the chunks.
Two writes to the socket per client come out of
MinecraftServer.tickChildren: one after the levels, one after the chunks. A
reply sent while the packets are drained before it, or from a task after it,
is flushed on its own.
The pacing of that second write belongs to what the client is
told;
which chunks are eligible to be in it, to tickets and
loading.
PlayerChunkSender
answers to the client’s own acknowledgements, so a slow client throttles its
own chunks without slowing the tick.
The bookkeeping at the bottom
MinecraftServer.tickServer closes with three pieces of bookkeeping. The cached
ServerStatus is rebuilt when the old one is more than
MinecraftServer.STATUS_EXPIRE_TIME_NANOS (five seconds) old, so a ping never
costs a walk of the player list. MinecraftServer.ticksUntilAutosave counts
down to MinecraftServer.autoSave, and it counts ticks while promising
minutes: it starts at MinecraftServer.AUTOSAVE_INTERVAL (6000) and is
thereafter MinecraftServer.computeNextAutosaveInterval, the tick rate times
300, floored at MinecraftServer.MIMINUM_AUTOSAVE_TICKS (100 — the typo is
Mojang’s). So an autosave is five minutes of the server’s scheduled time at any steady /tick rate, and at any set rate the floor never binds,
since the rate cannot go below 1; MinecraftServer.onTickRateChanged re-derives the figure
whenever that changes, but only ever shortens the pending countdown, and a sprint
uses the measured rate from MinecraftServer.getAverageTickTimeNanos so that
it saves at the speed it is really running. And the tick’s own duration replaces its
slot in the hundred-entry MinecraftServer.tickTimesNanos ring, updates
MinecraftServer.aggregatedTickTimesNanos, and folds into
MinecraftServer.smoothedTickTimeMillis at
MinecraftServer.AVERAGE_TICK_TIME_SMOOTHING. That ring is what /tick query
reads, through MinecraftServer.getAverageTickTimeNanos and
MinecraftServer.getTickTimesNanos.
The debug screen’s TPS chart is a different pipe: a SampleLogger with one
slot per TpsDebugDimensions value, written from three points of the loop.
MinecraftServer.logTickMethodTime records the tick method,
MinecraftServer.finishMeasuringTaskExecutionTime the scheduled-task and idle
slots after the wait, and MinecraftServer.logFullTickTime at the very bottom
both flushes the four-slot sample and measures the whole iteration, tick
plus wait. IntegratedServer logs it always; a dedicated server only while a
client is subscribed to DebugSubscriptions.DEDICATED_SERVER_TICK_TIME.
The loop is instrumented in three more places, none of them that ring.
Every iteration gets a fresh ProfilerFiller from
MinecraftServer.createProfiler, composing the MetricsRecorder’s profiler
with a SingleTickProfiler, and /debug start arms
MinecraftServer.TimeProfiler at the top of an iteration through
MinecraftServer.debugCommandProfilerDelayStart. MinecraftServer.tickFrame
is a DiscontinuousFrame from TracyClient, opened before the packet drain
and closed after the tick. JvmProfiler is fed
MinecraftServer.smoothedTickTimeMillis after the wait, on the last line of
the iteration — where MinecraftServer.isReady is also set, every lap rather
than once.
The event loop, and what a tick’s spare time buys
MinecraftServer extends ReentrantBlockableEventLoop of TickTask: it is
an Executor whose queue drains on the Server thread, and submitting a task to
it is how most other threads touch server state — a packet takes the
processor’s queue instead, and a console line the console’s list. This section
is where the rest of the book sends you for that machinery.
flowchart TD
P["BlockableEventLoop.pollTask peeks the head"] --> E{"anything queued"}
E -- "a task" --> B{"blocking depth above zero"}
B -- "inside BlockableEventLoop.managedBlock" --> RUN["run it, report true"]
B -- "not blocked" --> S{"MinecraftServer.shouldRun"}
S -- "older than three ticks" --> RUN
S -- "otherwise" --> H{"MinecraftServer.haveTime"}
H -- "time left" --> RUN
H -- "out of time, left queued" --> OFF["nothing ran from the server's own queue"]
E -- "nothing queued" --> OFF
The one question the head of the queue is asked, and the three independent reasons the answer is yes; the two edges into the bottom box are the ones the next figure picks up.
Read the bottom box twice. An empty queue and a queue whose head may not run
yet are the same answer to MinecraftServer.pollTaskInternal, and that answer
is the first of the two things a level’s chunk source waits for; the guard in
the next figure is the second.
Every runnable becomes a TickTask
MinecraftServer.wrapRunnable stamps the current MinecraftServer.tickCount
onto every task queued for the server, from whichever thread. That stamp is
the whole of a task’s identity to the scheduler: MinecraftServer.shouldRun
lets a task run when there is time left, or when it is older than
MinecraftServer.MAX_TICK_LATENCY (three) ticks — so a saturated server still
drains its queue, late but in order and without unbounded growth. Submitting
from the Server thread runs the work inline unless a task is already running:
ReentrantBlockableEventLoop.scheduleExecutables reports true then, so
re-entrant work queues instead of nesting. MinecraftServer.scheduleExecutables
and MinecraftServer.executeIfPossible, the two doors into the queue, both
answer differently once the server has stopped, which is the first move of how a server
dies.
A task that throws an exception is not the loop’s problem either.
BlockableEventLoop.doRunTask logs the failure under the fatal marker and
returns, rethrowing only what BlockableEventLoop.isNonRecoverable calls
unrecoverable — a ReportedException wrapping an OutOfMemoryError or a
StackOverflowError — while an Error thrown bare passes its catch untouched.
On a dedicated server a worker thread that dies surfaces here too, as a throw
out of BlockableEventLoop.pollTask rather than out of anything the tick
called: that is the relay how a server
dies describes, and what it becomes — the crash report, the shutdown the loop’s
finally performs, and the watchdog that reads
MinecraftServer.getNextTickTime from outside the loop and halts the JVM
without saving — is that page’s whole subject.
The budget, and where it stops applying
MinecraftServer.haveTime is the BooleanSupplier the whole tick is handed.
It is true unconditionally while a task is running
(ReentrantBlockableEventLoop.runningTask), and otherwise compares the clock
against MinecraftServer.delayedTasksMaxNextTickTimeNanos or
MinecraftServer.nextTickTimeNanos depending on
MinecraftServer.mayHaveDelayedTasks — which MinecraftServer.runServer sets
true, together with the slack deadline, immediately after the tick, and which
every subsequent MinecraftServer.pollTask overwrites with was there more.
It stops applying the moment the thread blocks. Inside
BlockableEventLoop.managedBlock the blocking depth is non-zero, so
BlockableEventLoop.shouldRunAllTasks is true and
BlockableEventLoop.pollTask never consults MinecraftServer.shouldRun:
every queued task runs, budget and age irrelevant. A level that blocks on a
chunk mid-tick does the same on its own ServerChunkCache.MainThreadExecutor,
whose tasks always run, and does not deadlock for the same reason — the wait
is the drain, and the thread doing the waiting is the thread that completes
the thing it waits for. MinecraftServer.waitUntilNextTick is the mechanism
used deliberately: BlockableEventLoop.runAllTasks, then
BlockableEventLoop.managedBlock on no time left.
MinecraftServer.waitForTasks parks with LockSupport until
MinecraftServer.nextTickTimeNanos — or 100 µs at a time when the loop is not
waiting on a tick — and BlockableEventLoop.schedule unparks it, so a
submitted task cuts the park short. PacketProcessor.scheduleIfPossible
pointedly does not: a packet landing in the slack waits for the next drain.
What the budget actually gates
Three things are all MinecraftServer.haveTime decides inside the tick,
once it has travelled from MinecraftServer.tickServer through
MinecraftServer.tickChildren, ServerLevel.tick, ServerChunkCache.tick
and ChunkMap.tick.
They are ChunkMap.processUnloads (the unload queue, which drains anyway
while it holds more than two thousand entries), ChunkMap.saveChunksEagerly
(at most twenty chunks a tick, and only under 128 outstanding writes) and
SectionStorage.tick by way of PoiManager.tick (the village-point data of
dirty chunks being written out). Loading a chunk, generating one, propagating
tickets and ticking chunks take no supplier and are not gated at all. A late
server does not load fewer chunks, then; it postpones unloading and saving
them, and its memory grows while it is behind.
The slack, and the sprint that inverts it
After the tick, MinecraftServer.waitUntilNextTick spends the remaining
milliseconds. MinecraftServer.pollTaskInternal polls the server’s own queue
first and, only if that queue had nothing to run, offers the levels’
ServerChunkCache.MainThreadExecutor.pollTask a turn, one after another until
one runs something — when the loop is sprinting, or blocked, or still in time. The levels get the leftovers of the
leftovers.
flowchart TD
N["nothing ran from the server's own queue"] --> G{"sprinting, blocked or in time"}
G -- "no" --> W["report false, and MinecraftServer.waitForTasks parks"]
G -- "yes" --> C{"any level's chunk source with work"}
C -- "one had" --> CRUN["a chunk source ran, report true"]
C -- "none did" --> W
What the leftover milliseconds buy: the guard that has to pass before the levels are asked at all, and the two ways the poll still ends in a park.
That executor keeps a policy of its own: its ServerChunkCache.MainThreadExecutor.shouldRun is unconditionally
true, with no age rule, and its poll runs
ServerChunkCache.runDistanceManagerUpdates first and returns at once if that
did any work. The first thing leftover milliseconds buy is chunks changing
status; the light schedule and the one queued chunk task only happen on a poll
where the graphs were already quiet.
Sprinting inverts the arithmetic. MinecraftServer.processPacketsAndTick
hands MinecraftServer.tickServer a constant false instead of
MinecraftServer.haveTime, so unloading, eager saving and section flushing
stop for the length of the sprint — and yet ServerTickRateManager.isSprinting
is the first term of the condition guarding the chunk-source poll, so the
levels are offered the turn on every poll regardless. A sprint therefore does
more chunk work per wall-clock second than an ordinary server, not less,
while doing little of the housekeeping that would let the results reach the
disk — the exceptions being the unload queue, which the two-thousand rider
drains whether there is time or not, and the autosave, which keeps counting.
Questions players ask
Does freezing stop the server? It stops the world, not the loop.
MinecraftServer.tickCount still increments, connections still tick and
packets are still answered, so a frozen server is one you can log into, walk
about in and type commands at — TickRateManager.isEntityFrozen exempts
players and anything carrying one — while nothing around you moves.
Is the tick rate settable to anything? Between
TickRateManager.MIN_TICKRATE (1.0) and TickCommand.MAX_TICKRATE (10000).
Clients are told: ServerTickRateManager.setTickRate and
ServerTickRateManager.setFrozen broadcast a ClientboundTickingStatePacket,
ServerTickRateManager.stepGameIfPaused a ClientboundTickingStepPacket, and
ServerTickRateManager.updateJoiningPlayer sends both to anyone arriving. A
sprint is not announced as such — a client is sent an unfrozen state as it starts
and, if the game was frozen, the refreeze after.
Why is an empty Nether nearly free? Because a dimension that no player’s
simulation ticket, forced chunk, portal or ender pearl keeps active stops
ticking entities, block entities and any dragon fight after 300 ticks of
ServerLevel.emptyTime, while still running its chunk-source work. That rule
belongs to the level
tick, and tickets and
loading owns which ticket
resets it.
Where to look
MinecraftServer.runServer · MinecraftServer.processPacketsAndTick ·
MinecraftServer.tickServer · MinecraftServer.tickChildren ·
MinecraftServer.waitUntilNextTick · MinecraftServer.haveTime ·
BlockableEventLoop · PacketProcessor · PacketUtils · TickRateManager ·
ServerTickRateManager · ServerConnectionListener.tick · Connection.tick ·
ServerCommonPacketListenerImpl.send · ServerChunkCache.MainThreadExecutor ·
IntegratedServer · DedicatedServer
Rules: names, never code · how the system works, not how the code reads ·
newest version only · every backticked name passes tools/verify_names.py.