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 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

classwhat it decidesthread
MinecraftServerthe loop, the thread and the event loop in one object: the deadline, the budget, and the order everything happens inServer
ServerTickRateManagerhow long this tick is, whether game elements advance at all, and whether the loop is sprintingServer
TickTaskone queued runnable plus the MinecraftServer.tickCount it was submitted on — the age the budget readsany thread submits, Server runs
PacketProcessorwhich serverbound packets are waiting, and that all of them are handled at the top of a tickNetty fills it, Server empties it
ServerLevelone dimension’s step — a lecture of its ownServer
ServerConnectionListenerwhich connections are still alive, and when each one’s player takes its own tickServer
ServerCommonPacketListenerImplwhether a packet handed to a player is written, or written and flushedServer, mostly
ServerChunkCache.MainThreadExecutorwhat the leftover milliseconds buy, one queue per levelServer

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 orderwhat it doesskipped when
suspend flushingServerCommonPacketListenerImpl.suspendFlushing on every player’s connectionnever
command functionsServerFunctionManager.tick runs ServerFunctionManager.LOAD_FUNCTION_TAG once after a reload, then ServerFunctionManager.TICK_FUNCTION_TAGfrozen
clocksServerClockManager.tick advances the world clocks, a SavedData kept in the world_clocks filefrozen, or GameRules.ADVANCE_TIME is off
time syncMinecraftServer.forceGameTimeSynchronization broadcasts a ClientboundSetTimePacketnot a multiple of 20 ticks
levelsMinecraftServer.updateEffectiveRespawnData, then ServerLevel.tick for each dimension in MinecraftServer.getAllLevels order, overworld firstnever
connectionMinecraftServer.tickConnection — every Connection, and each playing client’s own ticknever
playersPlayerList.tick broadcasts a latency-only ClientboundPlayerInfoUpdatePacketits own counter has not passed 600, so it fires on every 601st call
debug, game tests, tickablesServerDebugSubscribers.tick, GameTestTicker.tick, the dedicated server GUI’s refresh through MinecraftServer.addTickablegame tests alone, when frozen
send chunksPlayerChunkSender.sendNextChunks, then ServerCommonPacketListenerImpl.resumeFlushing, per playernever

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.