<!-- https://minecraftdocs.dev/systems/server/how-a-server-dies — this page as markdown. The book's index: https://minecraftdocs.dev/llms.txt; the whole book in one file: https://minecraftdocs.dev/llms-full.txt -->

# How a server dies

> Verified against **Minecraft 26.3** · Part III · `/stop` typed at the console, an exception out of the tick loop, and a tick that never ends — three endings, and what each writes of your world to disk.

An admin types `/stop`. The command sets one boolean and returns, and the
lap already in progress carries on to its end. Everything a player would
call *shutting down* — the players written, the unloads drained, `level.dat`
rotated, `session.lock` released — happens afterwards, inside the *finally*
of the loop that just exited, on the same Server thread that was ticking
mobs a moment ago. Which is what makes the second ending strange, and worth
a lecture: **a crash in the tick saves your world and the watchdog does not.** An
exception out of the tick loop lands in that same *finally*, so a server
that dies of a bad block entity writes exactly what `/stop` writes.
`ServerWatchdog` instead calls `System.exit`, which runs the Server Shutdown
Thread hook, which calls `MinecraftServer.halt` with *wait* true and waits
for the Server thread to finish — the very thread wedged in the tick that
tripped the watchdog. That wait never returns. Ten seconds later the
watchdog's own scheduled `Runtime.halt` ends the JVM with no save run.

## Three endings, side by side

|  | `/stop` | a crash in the tick | a watchdog kill |
|---|---|---|---|
| **what clears `MinecraftServer.running`** | `MinecraftServer.halt`, called by `StopCommand` with *wait* false and by five other callers below | nothing: the loop is left by the throw, not by the condition | the shutdown hook, eventually — `System.exit` runs it and it calls `MinecraftServer.halt` with *wait* true |
| **does `MinecraftServer.runServer`'s *finally* run** | yes, on the Server thread | yes, on the Server thread, after the crash report | no: the Server thread never leaves the tick |
| **are players saved** | yes, `PlayerList.saveAll` then `PlayerList.removeAll` | yes, identically | no |
| **are chunks saved** | yes: the unload drain, then `MinecraftServer.saveAllChunks` with *flush* | yes, identically | no — only what an autosave, an unload or the eager sweep had already written |
| **is `level.dat` written** | yes, `LevelStorageSource.LevelStorageAccess.saveDataTag` | yes, identically | no |
| **is `session.lock` released** | yes, `LevelStorageSource.LevelStorageAccess.close` drops the `DirectoryLock` | yes, identically | not by the game — the OS drops it when the process dies |
| **is a crash report written** | no | yes, into *crash-reports/*, from `MinecraftServer.constructOrExtractCrashReport` | yes, into *crash-reports/*, from `ServerWatchdog.createWatchdogCrashReport`, before the exit |
| **what ends the JVM** | nothing explicit: the Server thread returns and no non-daemon thread is left | the same | `Runtime.halt` from the watchdog's own timer, ten seconds after its `System.exit` |

The first two columns differ in three of the eight rows: what clears the
flag, when the *finally* runs relative to the crash report, and whether there
is a crash report at all. The third differs from the first in all eight, and
the rest of this page is why.

## The cast

| class | what it decides | thread |
|---|---|---|
| `MinecraftServer` | the two shutdown flags, the tick loop, and the *finally* that is the whole of shutdown | Server |
| `StopCommand` | that `/stop` is one call to `MinecraftServer.halt` with *wait* false, at `Commands.LEVEL_OWNERS` | Server |
| `DedicatedServer` | what wraps the base teardown: the JSON-RPC notification, `Util.shutdownExecutors`, and the side threads in `DedicatedServer.onServerExit` | Server |
| `ServerWatchdog` | that a tick past *max-tick-time* is a dead server, and that the JVM goes with it | Server Watchdog, a daemon |
| `PlayerList` | that every player is written before anyone is disconnected | Server |
| `ServerChunkCache` · `ChunkMap` | when the world is quiet enough to stop draining, and what a flush save means | Server, with the writes on the IO pool |
| `LevelStorageSource.LevelStorageAccess` | `level.dat` and the `DirectoryLock` on `session.lock` | Server |
| `Util` | the process-wide pools, and the three-second grace each of the two it shuts down gets | any |

## `/stop`, in full

```mermaid
sequenceDiagram
    participant MS as MinecraftServer
    participant PL as PlayerList
    participant SL as ServerLevel
    participant SCC as ServerChunkCache
    participant LSA as LevelStorageSource.<br/>LevelStorageAccess
    participant Disk

    Note over MS: StopCommand calls halt with wait false, so running goes false
    Note over MS: the lap in progress finishes, then the loop condition fails
    MS->>MS: stopped goes true, then stopServer, out of runServer's finally
    MS->>MS: PacketProcessor.close, then the connection listener is stopped
    MS->>PL: saveAll, then removeAll
    PL->>Disk: each player's dat file, stats and advancements
    MS->>SL: noSave cleared on every level
    loop while any ChunkMap.hasWork
        MS->>MS: the tick deadline pushed one millisecond out
        MS->>SCC: deactivateTicketsOnClosing, then tick, on every level
        MS->>MS: waitUntilNextTick drains the queue for what is left of it
    end
    rect rgba(0, 0, 0, 0.04)
        Note over MS,Disk: one call: MinecraftServer.saveAllChunks with flush
        MS->>SL: save, per level, reaching ChunkMap.saveAllChunks
        SL->>Disk: region files, entities, poi, and the chunk_tickets saved data
        MS->>LSA: saveDataTag, level.dat built into a temp file
        LSA->>Disk: the temp replaces level.dat, the old rotated to level.dat_old
        MS->>MS: SavedDataStorage.<br/>saveAndJoin, after level.dat and not before
    end
    MS->>SL: close, ServerChunkCache.close then the entity manager
    MS->>LSA: close, releasing the DirectoryLock on session.lock
    MS->>MS: Util.shutdownExecutors, then onServerExit stops RCON and query
    Note over MS: the Server thread returns, and no non-daemon thread is left
```

*The whole of shutdown, which is one **finally** block. The shaded band is a
single call, and the three saves inside it are in that order because that call
puts them there — not because `MinecraftServer.stopServer` asks for them one at
a time.*

### The command is a flag

`StopCommand` registers one literal at `Commands.LEVEL_OWNERS`, sends
*commands.stop.stopping* and calls `MinecraftServer.halt` with *wait* false.
That call assigns `MinecraftServer.running` — volatile, and the loop's only
condition — and returns. Nothing else
happens on that line of the console: the rest of the lap runs out — a console
command runs in the connection phase, after every level has ticked, and a
player's between ticks — and the loop condition at the top of
`MinecraftServer.runServer` fails on the next pass.
Five other places on this side of the jar call the same method: the server
GUI's window-close listener and `Main`'s shutdown hook, both with *wait* true,
so that they block until the Server thread has finished; the JSON-RPC
management API's `MinecraftServerStateServiceImpl`, which takes the flag from
its caller; `ServerCommonPacketListenerImpl.onDisconnect`, which stops a
singleplayer server when its host logs out; and `GameTestServer`, which halts
itself when its test run is over. The client adds three more of its own.

Teardown itself is the loop's *finally*. `MinecraftServer.runServer` sets
`MinecraftServer.stopped`, the plain field other threads read through
`MinecraftServer.isStopped`, and calls `MinecraftServer.stopServer`, then calls
`MinecraftServer.onServerExit` from a nested *finally*, so that a teardown
which throws still stops the side threads. `DedicatedServer.stopServer`
wraps the base with `NotificationManager.serverShuttingDown` before and
`Util.shutdownExecutors` after.

### The front door closes, the guests do not leave

`PacketProcessor.close` is first, before anything is even logged. Afterwards
`PacketProcessor.scheduleIfPossible` refuses a packet a Netty thread has just
decoded, and `PacketProcessor.processQueuedPackets` returns without draining
— so packets already in the queue go the same way as the ones still
arriving. Then `ServerConnectionListener.stop` closes the channels it
*bound*, and only those: closing a Netty parent channel does not close the
connections accepted through it. Live sessions are severed after the players are saved, by
`PlayerList.removeAll`, with the *multiplayer.disconnect.server_shutdown*
reason. A connection still in handshake, login or configuration has no
`ServerPlayer` and is in neither list, so it is closed by neither, and simply
dies with the process ([protocol
phases](../networking/protocol-phases#configuration): a session exists only
once configuration is acknowledged).

`MinecraftServer.stopped` also changes how work is accepted, and the two
doors answer differently. `MinecraftServer.executeIfPossible` throws a
*RejectedExecutionException* outright. `MinecraftServer.scheduleExecutables`
merely reports false, so a caller reaching `BlockableEventLoop.execute` from
another thread runs its task inline on its own thread rather than queueing it
for a loop that has stopped looping ([the server
tick](server-tick#every-runnable-becomes-a-ticktask) is the same two doors
while the server runs).

### Players before chunks, and never `MinecraftServer.saveEverything`

Shutdown does not use the save entry point everything else uses.
`MinecraftServer.saveEverything` — autosave, `/save-all`, the integrated
server's pause, the JSON-RPC save call — is players *then* chunks in one
call. `MinecraftServer.stopServer` does the two halves by hand:
`PlayerList.saveAll` (each player's data through `PlayerDataStorage`, plus
their `ServerStatsCounter` and `PlayerAdvancements`), then
`PlayerList.removeAll`, and only much later `MinecraftServer.saveAllChunks`.
`PlayerList.removeAll` is thinner than its name: it disconnects each
connection and nothing else, so the tickets those players hold are *not* what
goes with them. What lets the next step finish is
`ServerChunkCache.deactivateTicketsOnClosing`, called on every level inside
the drain loop itself.

`ServerLevel.noSave` is then cleared on every level. `/save-off` does not
survive `/stop`.

### The drain

`ChunkMap.hasWork` is the question, and it is a broad one — nine things in
one *or*: pending light, pending unloads, a non-empty updating map, POI work,
chunks queued to drop, a non-empty unload queue, the worldgen and light dispatchers, and `DistanceManager.hasTickets`, the
reason the loop terminates at all. While any level answers yes, the server pushes
the tick deadline one millisecond out, calls
`ServerChunkCache.deactivateTicketsOnClosing` and `ServerChunkCache.tick` on
each level, and runs `MinecraftServer.waitUntilNextTick`, which drains the
main-thread queue and polls each level's chunk executor for what is left of
that millisecond.

**One millisecond** — each slice of the drain, so unloads and their saves
proceed while the main-thread queue keeps taking chunk results.

`TicketStorage.deactivateTicketsOnClosing` moves every ticket except
`TicketType.UNKNOWN` into a parked map — the flush save below writes the
persistent ones out as the dimension's *chunk_tickets* saved data, under
`TicketStorage.TYPE` — and that is what ends the loop:
`TicketStorage.hasTickets` counts only the live map, so parking a ticket stops
it holding a chunk. Parked is not forgotten — which of them survive to the
next boot, and what re-arms them there, belong to [tickets and
loading](../world/tickets-and-loading#what-a-ticket-asks-for) and
[starting a
server](starting-a-server#preparing-the-levels-which-prepares-nothing).

### The flush save

`MinecraftServer.saveAllChunks` with *flush* true is the real save. The
scoreboard is pushed into its saved data, then each `ServerLevel.save`:
`ServerLevel.saveLevelData` joins the level's own `SavedDataStorage`, and
`ServerChunkCache.save` runs the distance manager once more before
`ChunkMap.saveAllChunks` in flush mode. That last one is a *loop*, not a
pass — every holder that `ChunkHolder.wasAccessibleSinceLastSave`, waited on
with `BlockableEventLoop.managedBlock` until `ChunkHolder.isReadyForSaving`,
repeated until a whole round saves nothing new — and then
`SectionStorage.flushAll` for the POI sections, the unloads processed, and
`SimpleRegionStorage.synchronize` joined so that the `IOWorker` has actually
put the bytes down ([chunk
storage](../world/chunk-storage#the-four-moments-a-chunk-is-written)). Entities
follow, through the level's `PersistentEntitySectionManager`.

`level.dat` is written next, the same way at every save, flush or not —
`LevelStorageSource.LevelStorageAccess.saveDataTag`, a gzipped temp file
through `NbtIo.writeCompressed`, and a replace that keeps the previous copy ([level data and
rules](../../reference/level-data-and-rules#what-is-left-in-leveldat) has
the path and the retries). Only after that does the *server-wide*
`SavedDataStorage` get its `SavedDataStorage.saveAndJoin`. There are two tiers
of saved data, and they are flushed at opposite ends of this section.

### The closes, and the last thread

`ServerLevel.close` is `ServerChunkCache.close` — which saves once more, then
closes the level's saved data, the `ThreadedLevelLightEngine` and `ChunkMap`
— followed by the entity manager. Then the server's own `SavedDataStorage`
(whose `SavedDataStorage.close` is itself a final
`SavedDataStorage.saveAndJoin`), the `MinecraftServer.ReloadableResources`,
and last `LevelStorageSource.LevelStorageAccess.close`, which releases the
`DirectoryLock`. From that moment the world is openable by anything else.

`Util.shutdownExecutors` then stops `Util.backgroundExecutor` and
`Util.ioPool` with a three-second grace each, inside
`DedicatedServer.stopServer` and so *before* `DedicatedServer.onServerExit`
— nothing may need a worker after that point. `Util.nonCriticalIoPool` is
untouched, and survives only because its threads are daemons.
`DedicatedServer.onServerExit` closes the text filter and the GUI and stops
`RconThread`, `QueryThreadGs4` and the `ManagementServer`. There is no
`System.exit` anywhere on this path, and none is needed. Every other thread
the server started is a daemon — the console reader, the Netty groups, the
management server's group, the watchdog — except the RCON and query threads,
which `GenericThread.stop` joins here in one-second slices, and the IO
pool's workers, which went a step earlier with `Util.shutdownExecutors`, and the
chat filter's pool, whose workers the text filter's close shuts down, and, with the GUI up, Swing's event dispatch thread, which ends once `MinecraftServerGui.close` disposes the frame. So
when `MinecraftServer.runServer` returns, the Server thread is the last one
left, and the JVM ends because there is nothing to keep it
([the thread reference](../../reference/threads#the-threads-a-lecture-leans-on)).

## The crash that saves

`MinecraftServer.runServer` wraps the entire loop, `DedicatedServer.initServer`
included. Anything thrown out of a tick — a block entity, a mob's AI — is
logged, turned into a report, saved, and then falls into the same *finally*.
A packet handler's exception and a command's are caught before they get here;
what escapes is an *Error*, or, from a packet handler, a `ReportedException`
carrying an *OutOfMemoryError*.

`MinecraftServer.constructOrExtractCrashReport` walks the cause chain and
keeps the *innermost* `ReportedException` it finds, using that exception's
own report and noting the outer throwable under a *Wrapped in* category. A
throwable with no `ReportedException` anywhere in it becomes a fresh report
titled *Exception in server tick loop*. Either way
`MinecraftServer.fillSystemReport` fills it in — the value of
`MinecraftServer.running`, the player count and roster, the selected and
available data packs, the enabled feature flags, the world-generation
lifecycle, the world seed, and the contents of the server's
`SuppressedExceptionCollector`, which has been quietly watching every chunk
load failure, chunk save failure and packet-handler exception since boot —
keeping the latest eight (the message, the class and the place, without the
stack) and a count of every one by place and class.
`DedicatedServer.fillServerSystemReport` adds two lines, the modded
status and the words *Dedicated Server*. The file lands in *crash-reports/*
under `MinecraftServer.getServerDirectory`, named by
`Util.getFilenameFormattedDateTime`. `MinecraftServer.onServerCrash` is a
hook the dedicated server does not override.

**A crash on another thread arrives here too.** `BlockableEventLoop` keeps
one static parked report. `Util.onThreadException` — the uncaught-exception
handler on every `Util.backgroundExecutor` and IO-pool thread — and
`GenerationChunkHolder`, when a generation step completes exceptionally,
both call `BlockableEventLoop.relayDelayCrash`, which parks the report or
suppresses the new one under a report already parked. The next
`BlockableEventLoop.pollTask` on a loop constructed with crash propagation
throws it as a `ReportedException`, and the dedicated server is constructed
that way. So a worker that dies does not die silently: it dies as a
tick-loop crash, on the Server thread, at whatever moment that thread next
looks for a task ([the server
tick](server-tick#the-event-loop-and-what-a-ticks-spare-time-buys)). It does
not save like one. The parked report is never cleared, so the shutdown's own
drain throws it again the first time it waits for chunks: the players are
written, and so are the chunks the drain's first pass unloads, and then the
teardown stops before the flush save, leaving the entities, `level.dat` and
the saved data unwritten, the executors never shut down — the IO pool's
workers keep the JVM alive until they idle out — and the lock for the
operating system to drop. The integrated server
is constructed with propagation off and hands its report to the client
instead, through `IntegratedServer.onServerCrash`.

## The watchdog that does not

```mermaid
sequenceDiagram
    participant SW as ServerWatchdog
    participant MS as MinecraftServer
    participant JVM
    participant Hook as Server Shutdown Thread
    participant Disk

    Note over MS: wedged inside one tick, past max-tick-time
    SW->>MS: getNextTickTime, a deadline now far in the past
    SW->>SW: createWatchdog<br/>CrashReport<br/>dumps every thread
    SW->>MS: fillSystemReport, read off-thread while the tick still runs
    SW->>Disk: the report, to stdout and to crash-reports
    SW->>JVM: Runtime.halt armed for ten seconds from now
    SW->>JVM: System.exit
    JVM->>Hook: run the shutdown hooks
    Hook->>MS: halt with wait true, running becomes false
    Hook->>MS: then waits for the Server thread, which is the wedged one
    MS-->>Hook: nothing, because the tick never returns
    Note over SW,Disk: ten seconds later the armed halt fires, with no save run
```

*The only ending that waits on a thread that will never finish: the exit waits
for the hook, the hook for the wedged thread, and the armed halt is the only
thing that moves. The one message to the Disk lane is the difference between
this and a server that dies silently.*

`ServerWatchdog` is a daemon thread started by `DedicatedServer.initServer`
whenever `DedicatedServer.getMaxTickLength` is positive — that is
`DedicatedServerProperties.maxTickTime`, *max-tick-time*, default sixty
thousand milliseconds, and setting it to zero or less means the thread is
never created and there is no backstop at all. Its loop is short: while
`MinecraftServer.isRunning`, compare `Util.getNanos` with
`MinecraftServer.getNextTickTime`, then sleep exactly until the earliest
moment a violation could be true.

What it compares matters. `MinecraftServer.getNextTickTime` is the *deadline*
the loop set for the tick it is running, not a timestamp of when that tick
began, and the tick loop advances it before each tick and again when catching
up after an overload. The watchdog fires when the server is that far past
where it promised to be.

The report comes first, and it is the good part of the design.
`ServerWatchdog.createWatchdogCrashReport` dumps every thread in the JVM,
sorts them daemon-last, appends the lot as a *Thread Dump* category, and
grafts the Server thread's stack trace onto a synthetic error — so the
report's headline stack is the code that hung. `MinecraftServer.fillSystemReport`
adds the usual, and the watchdog then adds a *Performance stats* category
holding the random-tick game rule and `ServerLevel.getWatchdogStats` for every
level: players, the five commonest entity types and ticking block-entity
types, block and fluid tick counts, chunk source stats. All of it is read from another thread
with no lock on the world, off a world that is mid-tick — which is exactly the trade the
class makes, because the alternative is asking a wedged thread for the
answer. It goes to real stdout through `Bootstrap.realStdoutPrintln` and to
*crash-reports/* like any other report.

Then the wait that cannot end. The watchdog schedules `Runtime.halt` on a timer and calls
`System.exit`, which runs the registered shutdown hooks — including the
"Server Shutdown Thread" that `Main` registered at boot, whose whole body is
`MinecraftServer.halt` with *wait* true. That sets `MinecraftServer.running`
false, which the wedged tick will never read, and then waits for the Server
thread to end. It does not end. `System.exit` will not return until its hooks
do, so the JVM sits there until the watchdog's timer fires.

**Ten seconds** — from the watchdog's `System.exit` to its `Runtime.halt`
(`ServerWatchdog.MAX_SHUTDOWN_TIME`), and the Server thread writes nothing in
any of them; only writes already queued on `Util.ioPool` can still land.

The watchdog is a liveness backstop, and reading it as a safe stop gets the
guarantee backwards. Whether anything guards shutdown depends on how
shutdown was reached, because the watchdog loops while
`MinecraftServer.running` is true and only `MinecraftServer.halt` ever clears
that flag. After `/stop` it is cleared before the drain begins, so a server
stuck on "Saving chunks" is stuck with no watchdog left watching it. After a
crash nothing clears it — the crash path never calls `MinecraftServer.halt` —
so the watchdog
is still counting. The drain loop survives that by resetting the deadline
every pass; the flush save that follows it does not reset anything, so a slow
enough save after a crash can be shot by the watchdog mid-write.

## The endings that are `/stop` under another name

### Ctrl-C, SIGTERM and the window button

Ctrl-C at the console and a *SIGTERM* from a service manager are the same
thing as far as the game is concerned: the JVM runs its shutdown hooks, and
the one `Main` registered calls `MinecraftServer.halt` with *wait* true. The
contrast with the watchdog is in the health of the thread being waited for,
and in the halt nothing here has armed. Here the Server thread is fine, notices the cleared flag at the top of
its next lap, and runs the entire `/stop` teardown while the hook thread
waits. A Ctrl-C on a healthy server *is* a `/stop`, and the JVM does not exit
until the world is on disk. The server GUI's window-close button does the
same thing from the AWT thread.

### Singleplayer ends on a poll

`Minecraft.disconnect` — reached from *Save and
Quit*, from a disconnect, and from `Minecraft.emergencySave` — closes the
client's connection, then calls `IntegratedServer.halt` with *wait* false.
That override first uses `BlockableEventLoop.executeBlocking` to remove every
player who is not the host, then clears `MinecraftServer.running` and stops
the LAN pinger. The client then puts up a `GenericMessageScreen` reading
`Gui.SAVING_LEVEL` and calls `Minecraft.renderFrame` in a loop while
`MinecraftServer.isShutdown` is false. The "Saving world" screen is not a
progress bar and is not driven by the server: it is a render loop spinning on
one question, *is the Server thread dead yet*
([the client loop](../client/the-client-loop#starting-and-the-three-ways-of-stopping)). Closing the game window
reaches the same place by a different road: `Window.shouldClose` makes
`Minecraft.runTick` call `Minecraft.stop`, which ends the frame loop, and
`Main` then calls `Minecraft.exitWorldAndClose` on its way out. The client's
"Client Shutdown Thread" is a JVM shutdown hook rather than that path — the
backstop for a kill signal — and after an ordinary exit it finds
`Minecraft.singleplayerServer` already null.

`MinecraftServer.isShutdown` is the right question to spin on, and it is the
one fact in this teardown that is not a field: it asks whether the Server
thread is still alive, so nothing can set it and nothing can lie about it.
`MinecraftServer.isStopped`, which the screen could have watched instead,
goes true at the *start* of teardown, when nothing has been saved yet — a
"Saving world" screen driven by that one would come down before the world
was written.

The integrated server never calls `Util.shutdownExecutors`.
`IntegratedServer.stopServer` tears down published state and defers to the
base. The client owns those pools and shuts them down at the very end of its
own life, long after the world is closed.

## What you lose if you kill the process

### What an autosave has already written

Ordinary autosave is `MinecraftServer.saveEverything` with neither *flush*
nor *force*, on the countdown the tick keeps at the bottom of every lap
([the server tick](server-tick#the-bookkeeping-at-the-bottom)) — five
minutes of the server's scheduled time, whatever the tick rate. It writes every player, every
dirty chunk — under none of the per-chunk spacing that throttles ordinary
saving, because the sweep clears it on the way in ([chunk
storage](../world/chunk-storage#the-four-moments-a-chunk-is-written)) —
and, unconditionally, `level.dat`, followed by a scheduled write of the
server's `SavedData`. Between the two, the spawn point and the world time (in
`level.dat`) and the weather, the game rules and the world clocks (each its
own `SavedData` file — *weather*, *game_rules*, *world_clocks*) on disk are
never more than one autosave stale, even on a server nobody ever stops
cleanly. Everything else — a chest filled a moment ago, a mob that walked
into a new chunk, an inventory change — lives in the `LevelChunk`, the entity
sections and the player until something saves them; a changed chunk goes out
with the eager sweep once the tick has time and ten seconds have passed since
its last write ([chunk
storage](../world/chunk-storage#the-four-moments-a-chunk-is-written)).

### The honest answer, per ending

After `/stop` or a crash thrown by the tick itself,
nothing is lost: the drain, the flush save and the joined `IOWorker` mean the
process does not end until the bytes are down. The crash has one asterisk the
clean stop does not — the watchdog is still armed all the way through it — but
short of a save slow enough to trip it, both endings land the same. After a
watchdog kill, or a
*kill -9*, or a power cut, you lose everything no autosave, unload, eager sweep or logout had written, plus anything still queued inside the `IOWorker` — those writes run on
`Util.ioPool`, and `Runtime.halt` does not wait for a pool.

What you never lose is access to the world. The lock the boot took is an
operating-system one ([starting a
server](starting-a-server#taking-the-lock-and-fixing-leveldat-twice)), and
an operating system drops it when the process dies however it dies — so a
world left behind by a killed server opens on the next start.

### What you lose with no ending at all

Individual failures are quieter than any of this. A chunk that cannot be
written calls `MinecraftServer.reportChunkSaveFailure`: logged, added to the
`SuppressedExceptionCollector` that the next crash report will print, written
out as its own `ReportType.CHUNK_IO_ERROR` file under *debug/*, and followed
by a disk-space check. The tick does not stop, the server does not stop, and
the only signs at the time are a line in the log and that file, with a toast
as well in singleplayer.

## Where to look

The `/stop` road first, then the other two. `StopCommand` ·
`MinecraftServer.halt` · `MinecraftServer.runServer` (the loop and its
*finally*) · `MinecraftServer.stopServer` · `DedicatedServer.stopServer` ·
`ChunkMap.hasWork` · `MinecraftServer.saveAllChunks` ·
`LevelStorageSource.LevelStorageAccess.close` · `DedicatedServer.onServerExit` ·
`MinecraftServer.constructOrExtractCrashReport` ·
`BlockableEventLoop.relayDelayCrash` · `ServerWatchdog.run` ·
`Main` (the shutdown hook) · `IntegratedServer.halt` · `Minecraft.disconnect`

---

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