<!-- https://minecraftdocs.dev/systems/blocks/block-entities — 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 -->

# Block entities

> Verified against **Minecraft 26.3** · Part V · A furnace smelts raw iron while nobody is watching, and the player who opens it learns the fire from a block state and the arrow from a menu — never from the block entity itself.

You drop raw iron in the top slot of a furnace, coal in the bottom, and walk
away. Two hundred ticks later there is an iron ingot in a box nobody is
looking at. A block state is one of a fixed table, shared by every position
that has it, so the moment a position needs something of its own — an
inventory, a timer, a name — it gets a `BlockEntity`: a plain object owned by
its chunk, keyed by its position, created by the block and destroyed with it.
The surprise is how little that object says for itself. A furnace tells
nobody anything: `BlockEntity.getUpdatePacket` returns null for it,
`BlockEntity.setChanged` sends nothing at all, and the two things a player
*does* see — the fire in the world and the arrow in the GUI — are a block
state and four ints from a menu, both of which arrive on the tick **after**
the smelting step that produced them, because block entities tick in the
level's last content phase, after the broadcast and the players' ticks have gone by ([the
level tick](../server/server-level-tick#the-whole-tick-and-its-three-gates)).

## The cast

| class | what it decides | thread |
|---|---|---|
| `BlockEntity` | the position, the cached state, the components, and four defaults every subclass inherits: two for disk, and two for the network that both amount to *say nothing* | whichever thread owns the level |
| `BlockEntityType` | which blocks the entity is legal on, and what constructs it | immutable once the registry is built |
| `EntityBlock` | whether a block has an entity at all, which one, and which ticker *per level* | — |
| `LevelChunk` | the position-to-entity map, the ticker wrapper per position, and create / keep / replace / remove on every write | the chunk's owning thread |
| `Level` | the flat list of tickers and the two gates over it | Server thread, or the Render thread |
| `AbstractFurnaceBlockEntity` | three slots, four ints, a cached recipe check, and when the block's *lit* state has to change | Server thread only — its ticker is null on the client |
| `ChunkHolder` | which positions changed since the last drain, and the call to `BlockEntity.getUpdatePacket` for each broadcast position that has a block entity | Server thread, chunk-source phase |
| `FurnaceMenu` | what an open screen is allowed to see of all that: three slots and four ints | Server thread, mirrored on the client |

## A furnace tells nobody anything

`BlockEntity` has four hooks a subclass is expected to fill in, and they come
in two pairs. `BlockEntity.saveAdditional` and `BlockEntity.loadAdditional` are
the disk pair, and the base class's versions do nothing because what it saves of its own, the id, the position and the components, is written by the shells around them. `BlockEntity.getUpdateTag` and
`BlockEntity.getUpdatePacket` are the network pair, and *their* emptiness is a
decision rather than an absence: the tag comes back empty and the packet comes
back **null**. The base class declines to be synced, and a subclass that wants
to be must say so.

**Nineteen** classes say so, and every one of them answers with
`ClientboundBlockEntityDataPacket.create` of itself: signs, banners, beacons,
skulls, spawners and trial spawners, conduits, end gateways, structure and
jigsaw blocks, campfires, decorated pots, vaults, shelves, brushable blocks,
creaking hearts, copper golem statues and the two test blocks. Counting those
declarations in the decompile and mapping each class onto the **49**
registrations in `BlockEntityTypes` gives **twenty** synced types out of
forty-nine, because `HangingSignBlockEntity` is a type of its own that
inherits `SignBlockEntity`'s override and adds nothing.

The overriders of the packet and the overriders of the tag are not the same
list — there are nineteen of those too, and it is a different nineteen — and the
two classes that differ are instructive.
`PistonMovingBlockEntity` overrides `BlockEntity.getUpdateTag` but not the
packet, so its state travels only in a chunk send
([pistons and block events](pistons-and-block-events#the-write-nobody-is-told-about)).
`CopperGolemStatueBlockEntity` overrides the packet but not the tag, so what
it broadcasts is the base class's empty tag.

Everything else a client knows about a block entity (an operator's query, a command block's screen and the debug channel aside) it knows by consequence:
the block state it can see, a menu it has been given, and a block event — the
third channel, and the one that swings a chest lid without either side saying
what is inside ([pistons and block events](pistons-and-block-events#the-queue-and-which-tick-it-drains-in)). The
trace below is what the first two cost.

## One save hook, four ways out

Saving is a tree, not a chain, and the branch a caller picks decides how much
metadata rides along. Only `BlockEntity.saveAdditional` belongs to the
subclass; everything else is bookkeeping the base class adds.

| what runs | what it writes | who calls it |
|---|---|---|
| `BlockEntity.saveAdditional` | the subclass's own fields, and nothing else | nobody directly |
| `BlockEntity.saveCustomOnly` | that alone | thirteen of the tag overrides, the pick-block path, `/clone` and a few others |
| `BlockEntity.saveWithoutMetadata` | that plus *components* | the two below, plus `BlockInput`, the falling block and a few others |
| `BlockEntity.saveWithId` | that plus *id* | a structure template, which records a relative position of its own, and the adventure-mode block check's cache |
| `BlockEntity.saveWithFullMetadata` | that plus *id*, *x*, *y* and *z* | `LevelChunk.getBlockEntityNbtForSaving`, `/data`, the block predicates, a text component's block NBT and a few others |

Reading back is `BlockEntity.loadWithComponents` (fields plus components) or
`BlockEntity.loadCustomOnly` (fields only) over a `ValueInput`
([codecs, NBT and JSON](../foundations/codecs-nbt-json#save-code-never-sees-a-compoundtag)) — but something
has to decide *which class* to construct first, and that cannot come through
a `ValueInput`, because no entity exists yet to own one. So
`BlockEntity.loadStatic` reads *id* off the raw `CompoundTag` with
`BlockEntity.TYPE_CODEC`, calls `BlockEntityType.create`, and only then wraps
the same tag in a `ValueInput` and loads it. Any of those three steps failing
logs and returns null, and the position has no entity until a plain read builds
a blank one from the block state.

The network joins that path at the end rather than reusing it whole:
`ClientPacketListener.handleBlockEntityData` never reads *id* — it asks for the
entity by position *and* type, which a plain read builds from the block state if
it is missing, and hands the tag to `BlockEntity.loadWithComponents`. There is no separate network
deserialiser. Where the chunk's *block_entities* list is
written and read is [chunk storage](../world/chunk-storage#copy-on-the-server-encode-on-a-worker-write-on-the-io-lane).

## Create, keep, replace, remove

A block entity that appears because a *block* appeared is created and
destroyed inside one method: `LevelChunk.setBlockState`, after the section
write, the heightmaps and the light checks ([what a write
does](blocks-and-states#the-two-update-channels)). That is the lifecycle
path, and it makes two decisions — but in the opposite order to the one this
section is named for, because the old entity has to go before
`BlockBehaviour.BlockStateBase.onPlace` runs and the new one is built after it.
Removal first, then creation. It is not the only way one
comes into being: a chunk arriving from disk builds its entities from saved
tags, one arriving from the network from its block states and then their tags,
and `LevelChunk.getBlockEntity` in its *immediate*
mode constructs a missing one on a plain read — which is the mode every
`Level.getBlockEntity` asks for.

**Removal** happens only when the *block* changed, the old state had an
entity, and the new state does not claim it through
`BlockBehaviour.BlockStateBase.shouldChangedStateKeepBlockEntity` — which
exactly two blocks in 26.3 override, `CopperChestBlock` and
`CopperGolemStatueBlock`, both keeping the entity when the old state was
another block of the same family, so oxidising or waxing a copper chest does
not empty it. Removal is two halves with different gates. The side effects —
`BlockEntity.preRemoveSideEffects`, which for anything implementing
`Container` drops the contents through `Containers.dropContents` — run **only
on the server** and only with `Block.UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS`
clear. The bookkeeping, `LevelChunk.removeBlockEntity`, runs regardless —
though only the last of its steps is itself unconditional. The map entry going,
the game-event listener being unregistered, the debug view being told and the
entity being flagged removed all sit behind *is this chunk loaded in a level*,
which on the server a `LevelChunk` is not before its promotion finishes or once it is
unloading; the rebind of the ticker to `LevelChunk.NULL_TICKER` happens either
way.

**Creation** happens after `BlockBehaviour.BlockStateBase.onPlace`, and only
if the new state has a block entity and `BlockBehaviour.BlockStateBase.onPlace`
has not replaced the block. The chunk looks for
an existing one without creating it, and if what it finds does not pass
`BlockEntity.isValidBlockState` for the new state it logs a *mismatched block
entity* warning, removes it and builds a fresh one from
`EntityBlock.newBlockEntity` — the block's own factory, not
`BlockEntityType.create`. Only a surviving match is kept, with its cached
state refreshed and `LevelChunk.updateBlockEntityTicker` re-asking the block
for a ticker. That is why flipping a furnace's *lit* property costs almost
nothing: same block, valid state, same object — a fresh
`LevelChunk.BoundTickingBlockEntity` rebound into the wrapper is the whole of
the expense.

Chunk load and unload use the ends of the same machinery.
`ChunkStatusTasks.full` runs `LevelChunk.runPostLoad` to turn the saved tags
into entities, then `LevelChunk.setLoaded` and
`LevelChunk.registerAllBlockEntitiesAfterLevelLoad`, which attaches listeners
and tickers to entities built before the chunk belonged to a level.
`ServerLevel.unload` calls `LevelChunk.clearAllBlockEntities`: every entity
flagged removed, every ticker pointed at `LevelChunk.NULL_TICKER`.

## Two hundred ticks nobody watches

```mermaid
sequenceDiagram
    participant SL as ServerLevel
    participant LC as LevelChunk
    participant AFBE as AbstractFurnace<br/>BlockEntity
    participant CH as ChunkHolder
    participant FM as FurnaceMenu
    participant CPL as ClientPacketListener
    Note over SL,CPL: tick N, chunk-source phase: the broadcast drain runs now, with nothing yet to send
    Note over SL,CPL: tick N, entities phase: the players' menus, with nothing new yet
    Note over SL,CPL: tick N, block-entities phase, the level's last content phase
    SL->>LC: LevelChunk.<br/>BoundTickingBlockEntity.tick
    LC->>AFBE: serverTick, quickCheck finds the smelting recipe
    Note over AFBE: fuel consumed, both lit fields take 1600, the cooking timer at one
    AFBE->>SL: setBlock, LIT true under flags 3
    SL->>LC: setBlockState, same block, the entity kept and the ticker rebound
    SL->>CH: blockChanged, which only marks the holder dirty
    AFBE->>SL: updateNeighbourForOutputSignal, the comparator poke
    Note over SL,CPL: tick N plus 1, chunk-source phase, the broadcast drain
    CH->>CPL: ClientboundBlockUpdatePacket, the fire appears
    Note over CH: BlockEntity.<br/>getUpdatePacket<br/>answers nothing
    Note over SL,CPL: tick N plus 1, entities phase, the players tick
    Note over CH,FM: AbstractContainerMenu.<br/>broadcastChanges checks the four data slots
    FM->>CPL: ClientboundContainerSet<br/>DataPacket per changed slot
```

*Two consequences of one smelting tick, both a tick late because the block
entity ticks in the level's last content phase, after the drain that turns a
dirty holder into a packet and after the players whose ticks reconcile the
menu. The two routes out are the block state and the menu's four ints, and they
arrive by different machinery.*

The furnace's ticker is handed out by `AbstractFurnaceBlock.createFurnaceTicker`
**only when the level is a `ServerLevel`** — on the client it is null, so no
furnace anywhere ever ticks there. `AbstractFurnaceBlockEntity.serverTick` is
therefore the whole of smelting: burn down
`AbstractFurnaceBlockEntity.litTimeRemaining`, ask
`AbstractFurnaceBlockEntity.quickCheck` (a `RecipeManager.CachedCheck`, which
retries last tick's match before scanning the type) for a recipe on the input
slot, check that the result slot can take the output, and, if the fire is out
but fuel is present, light it: both lit fields take
`AbstractFurnaceBlockEntity.getBurnDuration` for that item — what its
`DataComponents.COOKING_FUEL` component resolves to in this furnace, 1600 for
coal — and one fuel item is consumed. Then
`AbstractFurnaceBlockEntity.cookingTimer` advances by one. Where the recipe
comes from is [recipes](../items/recipes#loading-one-scan-one-swap-and-four-indexes-built-later).

The two hundred is the recipe's number rather than the furnace's:
`AbstractFurnaceBlockEntity.cookingTotalTime` holds the recipe's *cookingtime*
— **200** in raw iron's recipe, a field every cooking recipe must name —
divided by the speed multiplier the fuel sets, which is one in a furnace. Nor is the timer a stopwatch that
merely stops. With the fire out and either slot empty it runs *backwards*, two a
tick, clamped at zero, so an interrupted smelt loses its progress in half the
time it took to earn it.

Two writes leave the block entity, and neither leaves the server this tick.
The first is the fire: lit-ness is a *block state*, so the ticker calls
`Level.setBlock` on its own position with `AbstractFurnaceBlock.LIT` flipped.
`ServerChunkCache.blockChanged` marks the holder dirty — and the drain that
turns dirty holders into packets, `ServerChunkCache.broadcastChangedChunks`,
lives in the chunk-source phase, which ran before entities and long before
block entities. The second is `BlockEntity.setChanged`, which the tick calls
only when it lit, finished or went out, as it did here: it marks the chunk
unsaved and pokes comparators through
`Level.updateNeighbourForOutputSignal` — the unconditional half of that walk,
and the reason a comparator notices a hopper filling a chest ([one int, and the
fan-out that exists to deliver
it](diodes-and-observers#one-int-and-the-fan-out-that-exists-to-deliver-it))
— and that is all it does. It sends nothing.

So a viewer sees both a tick late, by two different routes. Next tick's drain
sends the `ClientboundBlockUpdatePacket` and then — for every broadcast
position whose state has a block entity, including each position inside a
`ClientboundSectionBlocksUpdatePacket` — calls `BlockEntity.getUpdatePacket`,
one of the game's two call sites (the other resends the blocks under a player
the server thinks is standing on air), and gets null from the furnace. Next tick's
entity phase runs `ServerPlayer.tick`, which runs
`AbstractContainerMenu.broadcastChanges`, which compares the menu's four data
slots against the values last sent and emits a
`ClientboundContainerSetDataPacket` per difference. Those four ints are the
furnace's entire GUI: the flame is data 0 over data 1 and the arrow data 2
over data 3, read by `AbstractFurnaceMenu.getLitProgress` and
`AbstractFurnaceMenu.getBurnProgress`. The tick that lights the fire moves
three of them, because data 1 is the fuel's total burn duration; after that
only 0 and 2 change, so a steady smelt is two packets a tick per open screen
and none at all with no viewer.
How a menu is opened, synchronised and closed is
[containers and menus](../items/containers-and-menus#the-chest-you-see-is-not-the-chest).

On the fire's two-hundredth tick, `AbstractFurnaceBlockEntity.burn` moves the ingot into the result
slot and `AbstractFurnaceBlockEntity.setRecipeUsed` adds one to a counter map
— not to a recipe object, which is why
`AbstractFurnaceBlockEntity.getRecipeUsed` returns null. The experience is
paid out on collection: `FurnaceResultSlot.checkTakeAchievements`, reached
from a take *or* a shift-click, calls
`AbstractFurnaceBlockEntity.awardUsedRecipesAndPopExperience`, which pops the
orbs at the player and unlocks the recipes.

## Loaded is not enough to tick

Three things about that list are worth separating: the gates over it, the way
it maintains itself, and the fact that what a ticker does *between* calls is
nobody's business but its own.

### The two gates, and the distance that is not view distance

`Level.tickBlockEntities` walks one flat list, `Level.blockEntityTickers`,
under two gates the tickers themselves never see.
`TickRateManager.runsNormally` is the first, so `/tick freeze` stops every
block entity in the game. `Level.shouldTickBlocksAt` is the second, and it is
where the interesting asymmetry lives: on `Level` it is always true, on
`ServerLevel` it is `DistanceManager.inBlockTickingRange` — the
**simulation** chunk tracker, not the loading one ([tickets and
loading](../world/tickets-and-loading#the-number-line)). A chunk your view distance
keeps loaded and your simulation distance does not reach holds furnaces that
do not smelt, and nothing about the block entity records this: it is simply
never called. Below the gates,
`LevelChunk.BoundTickingBlockEntity` adds its own — not removed, adopted by a
level, inside the world border, and then, on a `ServerLevel` only, the chunk at
`FullChunkStatus.BLOCK_TICKING` with its entities loaded — before re-reading
the live state and ticking only while `BlockEntityType.isValid` still holds,
logging once and skipping while it does not.

### The list that prunes itself

The list is never searched. Removal rebinds the chunk's
`LevelChunk.RebindableTickingBlockEntityWrapper` to `LevelChunk.NULL_TICKER`,
whose `TickingBlockEntity.isRemoved` is permanently true, and the next pass of
`Level.tickBlockEntities` drops it on the way past — no search, on a list that
is walked every tick anyway. Additions made *during* the walk go to
`Level.pendingBlockEntityTickers` and are folded in at the top of the next
pass, so a block entity created by another block entity's tick starts ticking
one tick later. The client runs the same method from `Minecraft.tick`, after
its entity pass and before `ClientLevel.tick`, and only while unpaused.

### Three cadences the gates know nothing about

A ticker's cadence is its own business, and the hopper is the first worth
knowing: `HopperBlockEntity` moves one item and then sets a cooldown of eight of
its own ticks, the number `HopperBlockEntity.MOVE_ITEM_SPEED` names, so a hopper
chain runs at two and a half items a second at twenty ticks a second. The two
spawners are the other two cadences worth knowing, and they are the block
entities that keep their whole personality outside themselves.
`SpawnerBlockEntity` holds an anonymous `BaseSpawner` and hands it the tick;
the delay, the spawn count, the required player range and the rolled
`SpawnData` all live on the `BaseSpawner`, which is not a `BlockEntity` at all.
Both sides tick it, and they run different methods: `BaseSpawner.serverTick`
does the spawning, and `BaseSpawner.clientTick` runs its own copy of the
countdown to spin the cube and drop smoke and flame — a local animation, never
a packet, and both are gated on `BaseSpawner.isNearPlayer`, so a spawner with
nobody in range neither spins nor fires. `TrialSpawnerBlockEntity` is the same
arrangement with a state machine on top: its `TrialSpawner` holds a
`TrialSpawnerStateData`, the `TrialSpawnerState` it moves through is a property
of the block state, and the block entity holds the `TrialSpawner` ([entity
lifecycle](../entities/entity-lifecycle#the-other-ways-in) owns what either
one spawns and under which reason). Every other block entity in
the sub-package is one of the shapes on this page — four hooks, a ticker handed
out per level, the four save shells, and the create-keep-replace-remove
lifecycle — with different fields in the middle.

## Questions players ask

**Why doesn't the client know what is in a chest until I open it?**
`ChestBlockEntity` overrides neither sync hook, so a chunk send carries its
type and position with no tag at all (an empty update tag is sent as none)
and the client builds its chest from the *block state* the packet's sections
already decoded, with an empty container inside. What ticks on the client is
animation only: `ChestBlock.getTicker` hands out
`ChestBlockEntity.lidAnimateTick` on the client and null on the server, the
exact mirror of the furnace.

**Why does a shulker box keep its contents when every other container drops
them?** Because dropping is the *base class's* behaviour, not the block's:
`BlockEntity.preRemoveSideEffects` drops the contents of anything that
implements `Container`. Eight classes override that hook, and
`ShulkerBoxBlockEntity` is the one that overrides it to do nothing whatever.
The furnace overrides it too and calls the base version *first*, so it drops
like everything else and then pops the experience owed for uncollected smelts
at the block — awarding the recipes to nobody.

**Why does the arrow only move when the screen is open?** Because the arrow
is not a property of the furnace. It is data slot 2 of an
`AbstractContainerMenu` that exists only while a player has that screen open,
reconciled once per tick by the player who owns it. Close the screen and the
menu is gone, and the furnace goes on smelting with no packets at all.

## Where to look

Start with the two hooks that decide everything else:
`BlockEntity.getUpdatePacket` and `BlockEntity.setChanged`, neither of which
sends anything for a furnace. `BlockEntity.loadStatic` is the one place a class
is chosen from a tag, and `BlockEntity.preRemoveSideEffects` is the last word
before one is unregistered. The lifecycle is
`LevelChunk.setBlockState` with `LevelChunk.updateBlockEntityTicker` and
`LevelChunk.NULL_TICKER`, and the ticking is `Level.tickBlockEntities` over
`Level.shouldTickBlocksAt`. Then `AbstractFurnaceBlockEntity.serverTick` for
the smelt itself and `ChunkHolder.broadcastBlockEntity` for the call site that asks a block entity for a packet whenever its position is broadcast. `EntityBlock.getTicker` and
`BaseEntityBlock.createTickerHelper` are not named above and are the door into
how any block hands out a ticker at all.

How a block entity is *drawn* — and why a chest's block model is empty — is
[block-entity rendering](../rendering/block-entity-rendering#the-chests-block-model-is-empty-and-there-are-two-tables-of-them), in Part XI.

---

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