<!-- https://minecraftdocs.dev/systems/server/starting-a-server — 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 -->

# Starting a server

> Verified against **Minecraft 26.3** · Part III · Dropping the jar in an empty folder, typing *java -jar server.jar*, and waiting for the line that says *Done*.

The first run unpacks the libraries the jar carries, writes two files and
exits: you have not agreed to the EULA.
The second gets as far as *Preparing level "world"* and, a second or two
later, *Done (1.284s)! For help, type "help"*. By the time the second of
those lines prints, the server has opened a data pack stack, built every
registry the world is made of, taken an operating-system lock on the save
directory, rewritten `level.dat`, constructed a `ServerLevel` for every
dimension the packs declare and started listening on 25565 — and only the
levels were built between the two lines. Every other item on that list was
over before the first one printed, which is why the elapsed time *Done*
reports is so short. It is not a claim about the whole boot: query, RCON, the
watchdog and JMX are all started *after* that line, and the last of the
server's own start-up work happens later still. And the step in the middle
whose name promises the most —
`MinecraftServer.prepareLevels`, the one whose progress line still reads
*Preparing spawn area* — does the least: **on an ordinary world it loads no
chunks at all.** All it does is re-arm the chunk-loading tickets the last
shutdown wrote down ([tickets and
loading](../world/tickets-and-loading#what-a-ticket-asks-for) has the nine
kinds), and exactly two of the nine are ever written down.

## The cast

| class | what it decides | thread |
|---|---|---|
| `server/Main` | everything decided before the Server thread exists: the flags, the properties, the EULA, which directory is the world, and when to hand off | *ServerMain*, which the jar's bundler starts |
| `DedicatedServerSettings` | the live copy of `server.properties` — and the rewrite that happens every time a setting changes | *ServerMain*, then Server |
| `LevelStorageSource.LevelStorageAccess` | one open world: its directory layout, its `session.lock`, and every read and write of `level.dat` while the world is open | whoever holds it |
| `WorldLoader` | the packs, the registries and the datapack-driven resources, assembled into a `WorldStem` | *ServerMain* and `Util.backgroundExecutor` |
| `DedicatedServer` | what a dedicated server has and singleplayer does not: the console, the port, the legacy conversions, RCON, query, the watchdog | Server, but constructed on *ServerMain* |
| `MinecraftServer` | the levels, the saves and the loop — and `MinecraftServer.spin`, the line where the Server thread begins | Server |
| `ServerLevel` | one dimension: its chunk source, its saved data, its ticket store | Server |
| `LevelLoadListener` | what boot progress looks like: log lines on a dedicated server, a progress bar on a client | the caller's |

## From the command line to the Server thread

```mermaid
sequenceDiagram
    participant Main as Main
    participant LSA as LevelStorageSource.<br/>LevelStorageAccess
    participant WL as WorldLoader
    participant Worker as Worker
    participant DS as DedicatedServer

    Main->>Main: the flags, Bootstrap.bootStrap, server.properties, the EULA gate
    Main->>Main: JsonRpc.create, if the management server is enabled at all
    Main->>LSA: validateAndCreateAccess, taking session.lock in the constructor
    Main->>LSA: getUnfixedDataTagWithFallback, if there is a world: level.dat, or level.dat_old restored
    Main->>WL: load, wrapped in Util.blockUntilDone, so this thread is an executor
    WL->>Main: the pack-opening stage, the reload's apply steps and the final assembly, single-threaded
    WL->>Worker: static tags, world then dimension registries, the resources
    Worker-->>WL: the background stages, finished
    WL-->>Main: a WorldStem
    Main->>LSA: saveDataTag rewrites level.dat now, upgrade or no upgrade
    Main->>DS: MinecraftServer.spin runs the factory here, on this thread
    Note over Main,DS: the thread starts only now, and main returns
```

*The first half of the boot, and the wall at the foot of it: one thread drives
every line above that note, with the worker pool's help, and the server object
does not exist for most of them.*

Read the note bar as a wall. Above it one thread drives the work, handing the
load's background stages to the worker pool; below it *main* has returned, and everything that remains is the Server thread and the
daemons arranged around it — the half drawn under [*The Server thread wakes
up*](#the-server-thread-wakes-up-and-can-still-fail-twice) below.
[Anatomy](../anatomy/anatomy) draws the same hand-off from the client's
side, where the thread that spins the server is the one drawing frames.

## Everything *main* does before there is a second thread

`Main.main` does not run on the thread the JVM started with: the jar opens
with a bundler, which unpacks the libraries and the game's own jar into the
working directory and starts `Main.main` from that copy on a thread named
*ServerMain*, and that thread stays with it for all of what
follows. `SharedConstants.tryDetectVersion` reads *version.json* out of
the jar first, so everything downstream knows what version it is. Then the
flags are parsed — *--nogui*, *--port*, *--universe*, *--world*,
*--forceUpgrade*, *--recreateRegionFiles*, *--safeMode*, *--initSettings*,
*--pidFile* and the rest — and `CrashReport.preload` runs: `MemoryReserve`
sets a block of heap aside and one throwaway report is formatted and
discarded, so that the crash-report path is warm and has memory of its own
even when the reason for the crash is that there is none left.
`Bootstrap.bootStrap` and `Bootstrap.validate` build and freeze the static
registries ([identifiers and
registries](../foundations/identifiers-and-registries#before-the-game-exists)),
and `Util.startTimerHackThread` starts a daemon that sleeps forever and
touches nothing. The last thing `Bootstrap.bootStrap` does is quieter and
matters later: it puts a `LoggedPrintStream` over `System.out` and
`System.err` — a `DebugLoggedPrintStream` when debug logging is on — so that
anything printed from here on goes through the logger, and the original
stream survives only as `Bootstrap.STDOUT`. That is the stream a crash report
uses when the logger cannot be trusted ([how a server
dies](how-a-server-dies#the-watchdog-that-does-not)).

Only then does the server read its own configuration. `DedicatedServerSettings`
parses `server.properties`, and `DedicatedServerSettings.forceSave` writes it
straight back — which is why a properties file carried over from an older jar
comes back with the new keys filled in. `RegionFileVersion.configure` takes
the region compression out of it before any chunk file is ever opened
([chunk storage](../world/chunk-storage)). And
`Eula` reads `eula.txt`, or writes the default and reports it missing. That is
the gate: `Eula.hasAgreedToEULA` false means one log line and *main* returns.
No world has been opened, no server exists, and everything *main* has started
so far is a daemon — which does not hold the JVM open, so the process ends
with the return. *--initSettings* returns one step earlier still, having
written both files on purpose.

### The management port opens before the world does

With the EULA agreed, `Services.create` builds the authentication services and
the name cache in the universe directory, and `JsonRpc.create` starts a
`ManagementServer` if *management-server-enabled* is set — and throws, ending
the boot, if the secret it finds is not forty alphanumeric characters. A
*missing* secret is not that failure: `DedicatedServerProperties` resolves
*management-server-secret* to a freshly generated key when the file has none,
and the properties rewrite above puts that key in the file — so what stops a
boot is a malformed secret, not an absent one. TLS stops it too: it is on by
default and wants a keystore, which by default is not configured, so enabling
the management server and nothing else ends the boot. What starts is a Netty
WebSocket listener with its own event-loop group named *Management server
IO*, TLS on by default, and a
`JsonRpcNotificationService` registered on the `NotificationManager` that
everything later in the boot reports through. It binds before `session.lock`
is taken, and `DedicatedServer.onServerExit` stops it last, so the management
protocol is reachable at both ends of the server's life — including while
there is no world to ask it about ([how a server dies](how-a-server-dies)).

### Taking the lock, and fixing `level.dat` twice

`LevelStorageSource.createDefault` points at the universe directory — the
working directory unless *--universe* says otherwise — and
`LevelStorageSource.validateAndCreateAccess` opens the world named by
*--world* or by *level-name*. The validation is a symlink check; the lock is
neither optional nor deferred. `DirectoryLock.create`, called from the
`LevelStorageSource.LevelStorageAccess` constructor, opens
`DirectoryLock.LOCK_FILE`, writes a snowman into it and takes a
`FileChannel.tryLock` on it. That is an operating-system advisory lock, not a
value in the file: a second server on the same directory fails at once with
`DirectoryLock.LockException`, a crashed JVM releases it with nothing to clean
up, and a world folder copied out from under a running server carries a
`session.lock` that means nothing. The client's world list greys out a world
that `DirectoryLock.isLocked` reports held.

If the directory already holds world data, *main* parses `level.dat` once and
runs the result through the datafixers twice.
`LevelStorageSource.LevelStorageAccess.getUnfixedDataTagWithFallback` reads
the file and, on a parse failure, falls back to `level.dat_old` and restores
it over the original. That raw tag goes through
`LevelStorageSource.LevelStorageAccess.fixAndGetSummaryFromTag` for a
`LevelSummary`, which is enough to answer
`LevelSummary.requiresManualConversion` and `LevelSummary.isCompatible` — two
gates that each end the boot with one explanatory line — and separately
through `DataFixers.getFileFixer` for the fully upgraded tag the world is
built from.

### The world load turns the main thread into an executor

`ServerPacksSource.createPackRepository` builds the repository over the
world's *datapacks/* folder and `WorldLoader.load` does the rest: the staged
load that [the resource system](../foundations/resource-system) describes,
run here for server data. `WorldLoader.PackConfig.createResourceManager`
selects and opens the packs, `TagLoader.loadTagsForExistingRegistries`
collects tags for the static registries, `RegistryDataLoader` loads
`RegistryDataLoader.WORLD_REGISTRIES` and then
`RegistryDataLoader.DIMENSION_REGISTRIES`, the `WorldLoader.WorldDataSupplier`
turns the fixed `level.dat` into a `PrimaryLevelData` through
`LevelStorageSource.getLevelDataAndDimensions` — or, with no world data at
all, `Main.createNewWorldData` builds one out of `server.properties`. Last,
`ReloadableServerResources.loadResources` loads the rest:
`RegistryDataLoader.RELOADABLE_REGISTRIES`, the loot tables, recipes and
advancements among them, through `ReloadableServerRegistries.reload`, and then
the functions. What comes back is a `WorldStem`.

The threading is the part worth noticing. `WorldLoader.load` takes two
executors: `Util.backgroundExecutor` for the work, and a main-thread executor
for the stages that must be single-threaded. `Util.blockUntilDone` supplies
the second by handing the loader a queue's *add* method and then draining that
queue until the future completes. For the length of the world load
*ServerMain* is an event loop, running the pack-opening stage, the reload's
apply steps and the final assembly itself between bouts of waiting on the
workers.

Two things then happen before the server object exists. With *--forceUpgrade*
or *--recreateRegionFiles*, a `WorldUpgrader` rewrites the chunks that need it (an older version's, those
holding a cache *--eraseCache* clears, or every chunk with
*--recreateRegionFiles*) while *main* polls it once a second and logs
a percentage. And either way
`LevelStorageSource.LevelStorageAccess.saveDataTag` writes `level.dat` back
out, by the same replace every later save uses ([level data and
rules](../../reference/level-data-and-rules#what-is-left-in-leveldat)).
That is unconditional: a server started and killed one second later has
already rewritten its world data.

## `MinecraftServer.spin`, and the last thing *main* does

`MinecraftServer.spin` takes a factory rather than a server, and its order is
the point. It builds the `Thread` object first, sets priority 8 on a machine
with more than four processors, calls the factory *on the calling thread*, and
only then starts the thread — so the whole of the `DedicatedServer`
constructor runs on *ServerMain*, and the new thread cannot begin
before there is a server for it to run.

That constructor is real work. `MinecraftServer`'s own opens the server-wide
`SavedDataStorage`, takes the `WorldData` and `WorldGenSettings` out of the
stem ([creating a world](../worldgen/creating-a-world) is where the stem
was built, by a screen or by *server.properties*), builds the `ServerConnectionListener`, the `PlayerDataStorage`, the
`GameRules`, the `StructureTemplateManager` and the `PacketProcessor`,
finalises recipe loading, and refuses outright a stem whose `LevelStem`
registry has no overworld. `DedicatedServer`'s adds the `ServerTextFilter`,
the `ServerLinks` built from the bug-report property, and — when
*enable-code-of-conduct* is set — every *.txt* file under the *codeofconduct*
folder, which throws if that folder is missing. A misconfigured code of
conduct kills the server on *ServerMain*, before a Server thread exists to
be killed.

Back in *main*, the factory has already applied *--port*, *--demo* and
*--serverId* and, unless *--nogui* or a headless JVM says otherwise, opened
the Swing window through `DedicatedServer.showGui`. A *Server Shutdown Thread*
hook goes on the runtime — its whole body is
[one `MinecraftServer.halt` call](how-a-server-dies) — and *main* returns.

## The Server thread wakes up, and can still fail twice

```mermaid
sequenceDiagram
    participant DS as DedicatedServer
    participant MS as MinecraftServer
    participant SL as ServerLevel

    Note over DS,SL: the other side of the wall: the Server thread
    MS->>DS: runServer calls initServer
    DS->>DS: the console daemon thread, the properties into fields, the key pair
    DS->>DS: startTcpServerListener binds the port, then convertOldUsers
    DS->>MS: loadLevel, which is createLevels, forceDifficulty and prepareLevels
    MS->>SL: the overworld first, then one level per LevelStem on DerivedLevelData
    MS->>SL: the persisted tickets re-armed, then a wait in 10 ms slices
    SL-->>MS: on an ordinary world, nothing pending: nothing was asked for
    MS-->>DS: loadLevel returns
    DS->>DS: Done is logged here, before the loop is ever entered
    DS->>DS: the optional listeners, each if its property is set
    DS-->>MS: initServer returns true
    MS->>MS: the icon and the status response, then the tick loop
```

*The second half: the play port is bound two messages before the levels exist,
and the word **Done** is logged three messages before the loop it claims is
running.*

`MinecraftServer.runServer` is the Server thread's body and its first act is
`DedicatedServer.initServer`, which begins with the console: a daemon thread
named *Server console handler* reading `System.in` line by line. It runs
nothing itself. Each line becomes a `ConsoleInput` — the text plus a
`CommandSourceStack` built there on the console thread — appended to a
synchronized list that `DedicatedServer.handleConsoleInputs` drains from
`DedicatedServer.tickConnection`, so a typed command executes inside a tick,
in its connection phase.

Then the properties become fields: online mode, the local IP, the default game
type, the port. `MinecraftServer.initializeKeyPair` generates the RSA pair that
login encryption uses ([protocol
phases](../networking/protocol-phases#login)), and
`ServerConnectionListener.startTcpServerListener` binds the port with a Netty
server bootstrap. Two things can end the boot at this point, and they end it
identically.

| the failure | what the console says | the check |
|---|---|---|
| the port is already taken | *FAILED TO BIND TO PORT!* and the exception | `ServerConnectionListener.startTcpServerListener` throws, and `DedicatedServer.initServer` returns false |
| a legacy user list survived conversion | *FAILED TO START THE SERVER AFTER ACCOUNT CONVERSION!* and the files to delete by hand | `OldUsersConverter.areOldUserlistsRemoved` looks for *banned-players.txt*, *banned-ips.txt*, *ops.txt* and *white-list.txt*, and is false if any of the four is still there |

The conversion itself is `DedicatedServer.convertOldUsers`, which attempts
five migrations — the two ban lists, the op list, the whitelist and the player
save files — retrying a failed one up to twice more, five seconds apart, and reporting
true unless all five failed; a missing legacy file counts as a success, so an
ordinary server reports true on every boot. The gate is the separate check above, so
what stops the boot is the file nobody could convert. Either failure returns
false out of `DedicatedServer.initServer`, and a false there is not a quiet
exit: `MinecraftServer.runServer` calls it inside its own *try* and
throws an *IllegalStateException* on false, so the boot failure lands in the
loop's own catch, writes a crash report and falls into the same *finally* that
`/stop` reaches —
a server that failed to bind still walks the whole shutdown path and releases
`session.lock` on the way out ([how a server dies](how-a-server-dies)).

Past them, a `DedicatedPlayerList` is built, offline names are resolved in the
cache, and the log says *Preparing level "world"*.

## Building the levels

`MinecraftServer.loadLevel` is three calls: `MinecraftServer.createLevels`,
`MinecraftServer.forceDifficulty` — empty in the base class, and overridden
only by `DedicatedServer.forceDifficulty`, which pushes *server.properties*'
difficulty onto the world on every boot — and
`MinecraftServer.prepareLevels`.

`MinecraftServer.createLevels` builds the overworld first and by name, and it
is the only level that is special. It gets the custom spawners — the
`PhantomSpawner`, the `PatrolSpawner`, the `CatSpawner`, the `VillageSiege`
and the `WanderingTraderSpawner` — and its `ServerLevelData` is the real
`PrimaryLevelData` out of `level.dat`. With it in `MinecraftServer.levels` (a
`LinkedHashMap`, so it stays first for every later walk over the levels), the
scoreboard, the `CommandStorage` and the `Stopwatches` come out of the
server-wide saved data. Every other `LevelStem` in the registry then gets a
`ServerLevel` over a `DerivedLevelData`, which forwards the overworld's game
time, difficulty, game mode and hardcore flag and swallows every setter but
the spawn. That is why the difficulty is one number every dimension shares.
The other world-wide numbers a reader would expect to find here are not its
doing: the time of day belongs to `ServerClockManager`, the weather to one
server-wide `WeatherData`, and the spawn a level reports to
`MinecraftServer.effectiveRespawnData`
([level data and rules](../../reference/level-data-and-rules#the-spawn-every-level-reports-is-the-servers-not-each-levels)).

A brand-new world takes one detour, as soon as the overworld exists and
before the other dimensions do, and it is the detour that actually generates
terrain at boot. When `ServerLevelData.isInitialized` is false,
`MinecraftServer.setInitialSpawn` asks the generator for its origin chunk
(`ChunkGenerator.getOrigin`), reads the generator's spawn height, and walks a
spiral over the chunks five in each direction — the value
`MinecraftServer.SPAWN_POSITION_SEARCH_RADIUS` names — calling `PlayerSpawnFinder.getSpawnPosInChunk` until one of them offers a
standable block. The bonus chest is placed here if *--bonusChest* asked for
one. Then the flag is set, and no later boot of that world looks for a spawn again.

## Preparing the levels, which prepares nothing

`MinecraftServer.prepareLevels` replays a list. For each level a
`ChunkLoadCounter` records which chunks are already `ChunkStatus.FULL`, calls
`TicketStorage.activateAllDeactivatedTickets`, runs the distance manager again
and counts what is new. The tickets it replays are the persisting ones the last shutdown parked rather than dropped, and two of the nine `TicketType`s carry
`TicketType.FLAG_PERSIST` — `TicketType.FORCED`, from */forceload*, and
`TicketType.PORTAL` — so those two are the only entries a *chunk_tickets* file
contains ([tickets and
loading](../world/tickets-and-loading#what-a-ticket-asks-for) has the nine).
The world spawn is kept by nothing.

**Zero chunks** are loaded by `MinecraftServer.prepareLevels` on a world with
no forceloads and no live portal ticket.

What follows is `MinecraftServer.waitUntilNextTick` with the deadline set
`MinecraftServer.PREPARE_LEVELS_DEFAULT_DELAY_NANOS` — 10 ms — out, repeated
while `ChunkLoadCounter.pendingChunks` is above zero: the Server thread
pumping its own queue in slices, the same way a tick waits for a chunk
([the server tick](server-tick#the-budget-and-where-it-stops-applying)). With a total of zero it runs once and
leaves. `MinecraftServer.updateMobSpawningFlags` and the effective respawn
data are recomputed, and the step is over.

### What the console prints while nothing is prepared

The output is worth reading literally. `LoggingLevelLoadListener` logs
*Loading N persistent chunks...* when
`LevelLoadListener.Stage.LOAD_INITIAL_CHUNKS` starts and *Time elapsed: N ms*
when it finishes, with *Preparing spawn area: N%* — still the
*menu.preparingSpawn* string, its percentage computed by
`LevelLoadProgressTracker` — printed at most twice a second in between. On an
ordinary world there is no in between: N is zero, and the percentage line
prints at most once, on the loop's single pass. Of the four values `LevelLoadListener.Stage` declares,
`LevelLoadListener.Stage.PREPARE_GLOBAL_SPAWN` fires only on a world's first
boot, `LevelLoadListener.Stage.LOAD_PLAYER_CHUNKS` belongs to a player joining
rather than to boot — `PrepareSpawnTask`, in the configuration phase
([players and sessions](players-and-sessions#preparing-a-place-to-stand)) — and
`LevelLoadListener.Stage.START_SERVER` is declared and fired by nothing.

## *Done* comes before the loop

`DedicatedServer.initServer` logs *Done (1.284s)! For help, type "help"* the
moment `MinecraftServer.loadLevel` returns — and only then starts the optional
listeners. `QueryThreadGs4.create` runs if *enable-query*, `RconThread.create`
if *enable-rcon*, a *Server Watchdog* thread if
`DedicatedServer.getMaxTickLength` is above zero, JMX if
*enable-jmx-monitoring*. After those come one `MinecraftServer.saveEverything`
with flush and force and `NotificationManager.serverStarted` on the JSON-RPC
feed. `DedicatedServer.initServer` returns true, `MinecraftServer.runServer`
loads `server-icon.png` — the server directory's, or failing that the world's,
and it must be 64 by 64 — builds the first `ServerStatus`, and enters the tick
loop.

So *Done* is a claim about the world being ready rather than about the server
being reachable in every sense. The play port has been open since before the
level loaded, and RCON is still not listening when the line prints.
`MinecraftServer.isReady` — read by the singleplayer client's loading screen
and by the JSON-RPC status method — is set at the bottom of the loop's first
iteration, one tick later still.

## The threads startup leaves behind

Boot creates every thread on this list and then hands the process to one of
them; the column that matters here is *daemon*, because it decides which of
them can hold the JVM open. What each may touch is the thread reference's,
which has the complete set including the pools and the situational ones
([threads](../../reference/threads#the-threads-a-lecture-leans-on)).

| thread | made by | daemon | what it may touch |
|---|---|---|---|
| **Server thread** | `MinecraftServer.spin`, from *main* | no | everything — it is the only thread allowed to change the world |
| *Server console handler* | `DedicatedServer.initServer`, first statement | yes | `System.in` and the `ConsoleInput` list. The command itself runs on the Server thread |
| *Server Watchdog* | `DedicatedServer.initServer`, only if `DedicatedServer.getMaxTickLength` is above zero | yes | reads `MinecraftServer.getNextTickTime`, the game rules and `ServerLevel.getWatchdogStats` off-thread, mid-tick |
| *RCON Listener*, plus one *RCON Client* per connection | `RconThread.create`, after *Done* | no | TCP accept. `DedicatedServer.runCommand` hops the command onto the Server thread with `BlockableEventLoop.executeBlocking` |
| *Query Listener* | `QueryThreadGs4.create`, after *Done* | no | a UDP status protocol, read-only |
| *Management server IO* | `JsonRpc.create`, back in *main* | yes | the JSON-RPC socket. `ManagementServer.tick` runs from `DedicatedServer.tickServer` |
| *Timer hack thread* | `Util.startTimerHackThread`, in *main* | yes | nothing. It sleeps, and nothing wakes it |

Two rows carry a consequence for the other end of the story. `RconThread` and
`QueryThreadGs4` are the only non-daemon threads in this table besides the
Server thread itself — `Util.ioPool`'s *IO-Worker* threads, made outside it and
squarely in the boot path, are non-daemon too — so boot is where the process
acquires the three things that will later have to be stopped by hand rather
than abandoned, and what that costs at the other end belongs to [how a server
dies](how-a-server-dies#the-closes-and-the-last-thread). Why those two poll,
and the conditions that can leave RCON unstarted with *enable-rcon* set, are
per-thread facts and live with the rest of them
([threads](../../reference/threads#the-threads-a-lecture-leans-on)).

One more thing outlives boot without being a thread: `server.properties` stays
live. Nineteen `DedicatedServerProperties` fields are `Settings.MutableValue`s,
and `Settings.MutableValue.update` rebuilds the whole properties object and
`DedicatedServerSettings.update` writes the file back, so */whitelist*,
the spawn-protection setter and the JSON-RPC settings calls all edit
`server.properties` on disk while the server runs. */difficulty* does not: it
changes the world, and `DedicatedServer.forceDifficulty` puts the file's
difficulty back at the next boot.

## Singleplayer boots the same server with a shorter list

`IntegratedServer` uses the same `MinecraftServer.spin`,
`MinecraftServer.runServer` and `MinecraftServer.loadLevel`, and almost
nothing else on this page. `IntegratedServer.initServer` turns authentication
on, generates the key pair, calls `MinecraftServer.loadLevel`, sets the MOTD
from the host's name and the level name, saves once and returns true: no
console thread, no TCP bind, no legacy conversion, no RCON, no query, no
watchdog and no *Done* line. Its player list is built in the constructor
rather than in `DedicatedServer.initServer`, and its `LevelLoadListener` is a
`LevelLoadTracker` composed with the logging one, so the client's progress bar
and a dedicated server's log lines are two readings of the same callbacks. The
whole of it is constructed from `Minecraft.doWorldLoad` on the Render thread,
by a caller that goes back to drawing frames rather than returning from
*main* — [anatomy](../anatomy/anatomy) is that hand-off, and its diagram is
the one to read for how the two loops meet.

> **For a 1.21-era reader.** There is no *spawnChunkRadius* game rule:
> `GameRuleRegistryFix` deletes it out of any save that still carries one, and
> the chunks around the world spawn are kept alive by whoever stands in them
> and by nothing else. *spawn-protection* in `server.properties` survives and
> is unrelated — it is a permission check in
> `DedicatedServer.isUnderSpawnProtection`, not a loader.

## Where to look

In boot order. `server/Main` · `Bootstrap.bootStrap` · `Eula` ·
`DedicatedServerSettings` · `JsonRpc.create` ·
`LevelStorageSource.validateAndCreateAccess` ·
`LevelStorageSource.LevelStorageAccess` · `DirectoryLock.create` ·
`WorldLoader.load` · `Util.blockUntilDone` · `WorldStem` ·
`MinecraftServer.spin` · `MinecraftServer.runServer` ·
`DedicatedServer.initServer` · `MinecraftServer.loadLevel` ·
`MinecraftServer.createLevels` · `MinecraftServer.prepareLevels` ·
`LevelLoadListener` · `IntegratedServer.initServer`

---

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