Ticking & performance
How the server's tick loop actually spends its time, and how to write data packs that do not fight it — ticks, random ticks, spawn chunks, world seeds and the optimisation patterns that matter most.
The tick
The server advances the world in discrete steps called ticks, targeting 20 ticks per second — one tick every 50 ms. Everything the server does in a tick happens on one thread: block updates, entity AI, redstone, and every function your pack runs from the #minecraft:tick function tag.
If the work in a tick exceeds 50 ms, the server cannot keep up. It does not drop the work — it takes longer, and the measured rate falls below 20 TPS. This is why data-pack performance is really a question of how much work you add per tick, not how clever the logic is.
A tick is also the unit for /schedule, for time values, and for the age of most entities. 20 ticks = 1 second, 1200 ticks = 1 minute, 24000 ticks = one full Minecraft day.
What runs every tick
| Hook | When it runs | Cost model |
|---|---|---|
#minecraft:tick | Every tick, once per function in the tag | Paid 20× per second, forever — the most expensive place to put anything |
#minecraft:load | On world load and on /reload | Paid once; the right home for setup and constants |
/schedule | Once, after a delay | Lets you run at a lower frequency than every tick |
Most logic does not need to run at 20 Hz. A self-rescheduling function that runs every 20 ticks does 5% of the work of the same logic on #minecraft:tick, and for anything a player perceives as "immediate" a 2–4 tick period is usually indistinguishable.
data/example/function/slow_loop.mcfunction
# Runs once per second instead of 20 times per second.
function example:do_work
schedule function example:slow_loop 20t replace
Random ticks
Separately from the main loop, the game picks random blocks in each loaded chunk section and gives them a random tick. This is what grows crops, spreads grass, melts ice and decays leaves.
The rate is controlled by the randomTickSpeed game rule, which defaults to 3. That means 3 blocks are chosen per 16×16×16 chunk section per tick. The cost scales with the number of loaded chunks and with this value, so raising it is one of the easiest ways to make a server unplayable.
randomTickSpeed is multiplicative across every loaded section. Setting it to a large value to "speed up farms" applies that multiplier to the entire loaded world, not just the area you care about. Prefer targeted approaches — a function that grows specific crops — over a global rule change.
Spawn chunks & loading
A region around the world spawn point stays loaded whenever the server is running, whether or not any player is nearby. These are the spawn chunks. Historically they were the standard place to park always-running contraptions.
Loaded chunks are the main driver of per-tick cost: entities tick, block entities tick, and random ticks fire only in loaded chunks. Anything that increases the loaded set increases baseline load.
| Mechanism | Effect | Notes |
|---|---|---|
| Spawn chunks | Always loaded while the server runs | Size and ticking behaviour have changed across versions and are configurable on modern servers |
/forceload add | Keeps specified chunks loaded | The explicit, controllable modern alternative — you choose exactly which chunks |
| Player render/simulation distance | Loads chunks around each player | Scales with player count; the dominant term on a busy server |
Prefer /forceload over relying on spawn chunks. It is explicit, it is per-dimension, it can be removed when the pack is done with it, and it does not depend on where the world spawn happens to be. Always pair forceload add with a matching forceload remove so a pack does not permanently inflate the loaded set.
The world seed
The world seed is the number that deterministically drives terrain generation, structure placement and several random systems. The same seed with the same generation settings and the same version produces the same world.
Two consequences matter for pack authors:
- Determinism is version-bound. The same seed in a different version can generate different terrain, because generation code and worldgen data change between releases. A pack that assumes a specific structure at specific coordinates will break.
- The seed is not secret. Several mechanics leak enough information to recover it. Do not build anti-cheat or hidden-content logic on the assumption that players cannot know the seed.
For a pack that needs a known layout, generate the terrain from your own custom world generation settings or place structures explicitly, rather than depending on a vanilla seed reproducing.
Optimising a data pack
In rough order of how much they typically matter:
1. Run less often
The single largest lever. Move work off #minecraft:tick onto a scheduled loop, or trigger it from an event instead of polling for one. An advancement with a trigger, revoked and re-granted, detects an event at the moment it happens and costs nothing while waiting — a polling loop pays every tick to find nothing.
2. Narrow selectors before you use them
Selector cost scales with how many candidates the server must consider. Constrain by the cheapest dimension first — a bounded volume — before filtering on scores, NBT or predicates.
Two ways to find the same entities
# Considers every entity in the dimension, then filters.
execute as @e[type=minecraft:armor_stand,tag=marker] run ...
# Bounds the search to a small volume first.
execute positioned 100 64 100 as @e[type=minecraft:armor_stand,tag=marker,distance=..8] run ...
Always specify type= when you can, and add limit= when you only need one match. @e with no type is the most expensive selector in the game.
3. Keep entity counts down
Every entity ticks. Marker armour stands, item displays and area-effect clouds used as position markers are convenient but they are not free at scale. Where a pack only needs to remember a position, storing coordinates in scoreboards or storage costs nothing per tick, while a marker entity costs something every tick forever.
4. Prefer one chained execute to many commands
Each command has parsing and dispatch overhead. Folding conditions into a single execute chain avoids running later commands at all when an early condition fails, and avoids re-resolving the same selector repeatedly.
5. Treat macros as a cost, not a convenience
Macro lines 1.20.2+ are compiled when invoked with new arguments rather than being free substitutions. They are the right tool for genuinely dynamic commands, but a macro on a hot path that could have been a plain command is wasted work every tick.
6. Watch recursion and function-call depth
Recursive functions and long function chains multiply quickly. The maxCommandChainLength game rule caps some of this, but a pack that relies on approaching that cap is already doing too much per tick.
Measuring, not guessing
Performance intuition is frequently wrong. Measure before and after any optimisation.
| Tool | What it tells you |
|---|---|
/tick query 1.20.3+ | Current tick rate and average tick time |
/tick sprint / /tick freeze 1.20.3+ | Run the world fast or hold it still while you inspect state |
/debug start … /debug stop | Writes a profiling report identifying where tick time went |
| Server-side profilers | Third-party tools give per-function attribution on Paper-family servers |
To attribute cost to your own pack specifically, use /tick freeze to stop the world, then /tick step a fixed number of ticks with your pack's tick tag empty versus populated. The difference is your pack's cost, isolated from ambient world load.
The relative weights above are general guidance drawn from how the tick loop is structured, not measurements from a specific server. Command and selector costs vary with entity counts, dimension, server implementation (vanilla versus Paper-family) and version — which is exactly why the measuring step is not optional.