Minecraft server lag: finding and fixing the causes
Is it the server or the connection? Both feel identical but have completely different causes. Measure with TPS and Spark, pregenerate chunks and find the real culprit.
"The server is lagging" is the most common message in the ticket system of any game server operator, and at the same time the least useful one. Behind that single sentence sit at least three completely different technical problems that have nothing to do with each other and that are fixed by completely different means. Anyone who guesses instead of measuring spends weeks turning view-distance and Java flags, while the actual cause is a broken hopper chain in the basement of some player's build.
This article takes the route that actually works: first tell the symptoms apart, then measure, then find the cause, then apply the countermeasure. And at the end, the question most guides leave out: how you can tell that the problem is really fixed.
Two problems that feel exactly the same
There are two fundamentally different kinds of stutter, plus a third case that has nothing to do with the server at all.
Server lag means: the server no longer manages its 20 calculation steps per second. The world itself runs slower. Mobs stand still or twitch, furnaces take longer, mined blocks reappear, armor stands float with a delay.
Network lag means: the server calculates perfectly well, but the packets between player and server take too long or get lost. The player is pulled back while walking (rubberbanding), hits do not land, chat arrives late, but the mobs nearby move perfectly smoothly.
Client lag is the third case: too few frames per second on the player's own machine, usually caused by shaders, a high render distance in the client or too little RAM assigned. This is invisible on the server and cannot be fixed there either.
| Observation | Server lag | Network lag |
|---|---|---|
| Who is affected | all players at the same time | individual players, often from one region |
| Mobs nearby | twitch, freeze, teleport | move smoothly |
| Ping in the tab menu | normal | high or fluctuating |
| Console | "Can't keep up!" | quiet, possibly timeouts |
| TPS reading | below 20 | exactly 20 |
| Timing | reproducible under load | often in the evening, depends on time of day |
The one rule that is almost always right: if it hits everyone at once, it is the server. If it hits individuals, it is the connection. One special case breaks this rule: a completely overloaded server also acknowledges network packets too late and then produces higher pings on top of everything else. That is why you always measure both.
The first test takes 60 seconds
Connect to the server over SSH and open the server console. If the service is not yet running cleanly in the background, the article on starting a Minecraft server automatically will help.
On Paper, Purpur and Folia from 1.21 onwards, the Spark profiler is already part of the server, so nothing has to be installed. In game or in the console:
spark tps
spark health --memory --network
The output of spark tps shows four time windows (5 seconds, 1, 5 and 15 minutes) plus the tick durations as minimum, median, 95th percentile and maximum.
On plain Vanilla without plugins there is /tick query. The command reports the target rate and the average tick time. On top of that, F3 plus 2 shows operators a tick graph.
The console often gives the problem away by itself. These are the lines readers typically search for word for word:
[Server thread/WARN]: Can't keep up! Is the server overloaded? Running 2123ms or 42 ticks behind
[Server thread/WARN]: Can't keep up! Did the system time change, or is the server overloaded? Running 2340ms behind
This message simply means that the server needed considerably longer than the permitted 50 milliseconds for one calculation step and has to skip ticks. A single message after a restart or after world generation is normal. One every few minutes is a real problem.
At the same time, check the system side. mpstat is not part of the base installation on any of the systems we checked, it comes from the sysstat package and has to be installed once:
apt-get install -y sysstat procps
uptime
free -h
mpstat 1 3
On AlmaLinux, Rocky Linux and Oracle Linux the first line reads dnf -y install sysstat procps-ng iproute, because there the package that contains free, uptime, top and vmstat is not called procps but procps-ng.
In the output of mpstat, the interesting column is %steal. Values permanently above 5% mean that the virtual machine is waiting for CPU time that another guest is occupying. That is not a Minecraft problem then, it is a capacity problem of the underlying hardware.
One caveat about these three commands: on your own server or VPS they show exactly what you want to know. If your server runs inside a container instead, for example in a game panel such as Pterodactyl, then free, uptime, vmstat and mpstat report the values of the host system and not those of your instance. You are looking at someone else's load and someone else's memory. In that case, rely on the display of the panel and on spark health.
Reading TPS and MSPT correctly
A Minecraft server calculates 20 ticks per second, so every tick has a budget of 50 milliseconds. MSPT (milliseconds per tick) is the more meaningful value, because it shows problems before TPS drops at all. A server at 20.0 TPS and 46 ms MSPT runs at its absolute limit and tips over with the next player who enters a new area.
- MSPT below 30 ms: healthy, headroom available.
- MSPT 30 to 45 ms: tight, but playable. Act now.
- MSPT above 50 ms: TPS will inevitably drop and players will notice.
The distribution matters more than the average. A median of 18 ms with a maximum of 900 ms means spikes: individual expensive events such as the automatic save, chunk generation or a plugin task that runs every minute. A median of 60 ms, on the other hand, means constant load, so too much ticked content for the available processing power. The countermeasures are completely different.
One point hardware advisers like to skip: the main tick of Minecraft runs in a single thread. Chunk loading and networking are offloaded, the actual world simulation is not. A server with 32 cores and weak single-core performance is slower for Minecraft than one with 8 fast cores. More cores only help once several server instances run on the machine.
Spark: from suspicion to proof
Timings, the standard tool for years, has been set to an idle mode in Paper since the 1.21 series and no longer delivers usable data. Anyone still posting Timings links today is measuring nothing. Its successor is called Spark, it is built into Paper, it exists as a mod for Fabric, Forge and NeoForge, and as a separate variant for Velocity and BungeeCord.
The decisive approach: profile while the problem is happening. A profile of an empty server at three in the morning is worthless.
spark profiler start --timeout 300
spark profiler stop
For spikes that only occur occasionally, you filter out the bad ticks on purpose:
spark profiler start --only-ticks-over 60 --timeout 600
This records only ticks that took longer than 60 milliseconds. Those are exactly the ones players feel. The rest is ignored and does not add noise to the picture.
When reading the flame graph, beginners almost always make the same mistake: they look at the deepest branch. The right thing to look at is the width of the bars directly below the server tick. An entry at 3% is irrelevant, even if it goes a hundred levels deep. Rule of thumb: a single plugin above 15% of the tick time is a candidate. Very wide entries around block entities point to redstone and hoppers, wide entries around chunk loading point to world generation.
A note on confidentiality: the uploaded report is publicly accessible through its link and contains system information, paths, startup parameters and the complete plugin list. Only pass the link on to people you would trust with that information. With --save-to-file the profile stays local.
The usual suspects
Redstone and farms
Hoppers are by far the most expensive blocks in the game, because every single one checks on every tick whether something is lying above it. A sorting system with 400 hoppers costs more processing time than a hundred mobs. On top of that come observer clocks, which keep running even when nobody is nearby, as long as the chunk is being simulated. If Spark spends a striking amount of time in block entities, look for exactly these builds.
Entities
On Paper, /paper entity list lists the entities per world and per type together with chunk coordinates. That is the fastest route to the problem area. Typical finds: several thousand item stacks in a mob farm, a breeding pen with 300 cows, a forgotten area full of dropped arrows.
Sensible countermeasures in spigot.yml: lower the entity-activation-range for animals and monsters (for example animals from 32 to 16, monsters from 32 to 24) and raise the merge-radius for items and experience orbs slightly, so that fewer individual objects exist. In paper-world-defaults.yml, entity-per-chunk-save-limit caps how many objects of one type are saved per chunk in the first place.
Chunk loading
A player with an elytra or on a fast horse forces the server to generate new terrain continuously. World generation is the most expensive single operation there is. The same happens on the first trip into the Nether or after the world border has been widened. The fix is not a configuration value but pregeneration, and that is important enough for its own section further down.
Plugins
If Spark names a plugin, the case is clear. If it does not, only bisection helps: disable half of the plugins, measure, then keep halving whichever half carries the load. With 32 plugins that means five restarts instead of 32. Make a backup first, because plugins that write world data can leave content behind when they are removed that no longer works without them.
Pregenerate chunks before players walk on them
This is the single most effective measure in this whole article, and it is the one most often overlooked. The reason lies in the way Minecraft works: loading an already generated chunk from disk costs almost nothing. Generating a new chunk, on the other hand, means height map, biomes, ore distribution, caves, structures and lighting calculation, and a large part of that runs in the main thread. Exactly where the 20 ticks per second have to be produced as well.
That is why things typically stutter when someone takes off with an elytra or builds a rail line into nowhere: the server generates world while it is supposed to be running the game at the same time. Generating the world once in advance, out to the world border, turns expensive computation into cheap reading from disk.
Chunky, the tool of choice
Chunky is the pregenerator in common use today and runs as a plugin on Paper, Spigot and Purpur as well as a mod on Fabric and Forge. The procedure is the same on all platforms:
/chunky world world
/chunky center 0 0
/chunky radius 5000
/chunky start
The radius is given in blocks and should match the world border. Chunky reports progress and estimated remaining time by itself; with /chunky pause and /chunky continue the run can be stopped and resumed at any time, even across a restart.
The Nether and the End are separate worlds and have to be pregenerated individually. On Paper and Spigot they are named like this by default:
/chunky world world_nether
/chunky radius 1000
/chunky start
A smaller radius is enough for the Nether, because one block there corresponds to eight blocks in the Overworld. A Nether radius of 1000 therefore covers 8000 blocks of the Overworld.
What you have to plan for
- Time. A radius of 5000 blocks is roughly 78 million blocks of area. Depending on hardware, modpack and world generator, this takes anywhere from an hour to several days. Modpacks with their own world generators are considerably slower than Vanilla.
- Storage. A pregenerated world quickly takes up several gigabytes. Check how much is free beforehand, otherwise the disk fills up during the run, and that hits the server harder than any amount of stutter. How to check and clean up is described in Disk full: finding and freeing up storage.
- Timing. Pregenerate with an empty server, not during live operation. The tick rate is poor during the run, as expected, and that is not a fault. The whole point is to take this load out of playing time.
- Set a world border. Without a border, sooner or later somebody walks past the pregenerated area and everything starts again from scratch. Set the border to the same radius you pregenerated.
Cleaning up afterwards
If the world has already grown and contains areas nobody visits any more, Chunky removes the chunks outside the world border:
/chunky trim
That shrinks the world noticeably, and the backups with it. Make a backup first, without exception, because deleted chunks are regenerated the next time somebody enters them and everything players had built there is lost.
When Chunky is not an option
On older servers you will often still find WorldBorder with /wb fill. It serves the same purpose, but the project has barely been maintained for years, and for current server versions Chunky is the more reliable choice. In both cases, check before installing whether the version on offer really matches your server version.
view-distance and simulation-distance
These two values in server.properties get confused constantly.
view-distancedetermines how far chunks are sent to the client. Costs bandwidth and a little memory.simulation-distancedetermines how far the server calculates entities, redstone and block updates. Costs CPU time in the main tick.
Chunks between the simulation limit and the view limit are displayed, but frozen on the server side. The cost grows quadratically: at view-distance=10 that is 21 by 21, so 441 chunks per player.
Proven starting values for a survival network with 10 to 30 players:
view-distance=8
simulation-distance=5
If you still come across guides that mention no-tick-view-distance: this Paper option is obsolete. Mojang introduced simulation-distance in 1.18 and solved the same problem officially. Setting the old value today achieves nothing.
The price of low values is rarely mentioned: below a simulation-distance of 4, AFK farms break down, mob spawns behave differently, furnaces in the neighboring chunk stop running and redstone clocks stand still. If your community is farm-heavy, you are saving in the wrong place here and trading a technical problem for a player problem. Move in steps of one and measure after every step.
Cleaning up without destroying the world
Every intervention in the world data starts with a backup taken while the server is stopped. No compromise, no exception:
tar -czf world-backup.tar.gz world world_nether world_the_end
After that it is worth looking at the size of the region data:
du -sh world/region
Worlds that have grown over years often contain hundreds of megabytes of terrain that a single player flew over once. These chunks cost no tick time, but they do cost disk space and they make every save operation longer. If space gets tight, the article Cleaning up a full disk on Linux helps as well.
Regular spikes in the range of seconds that occur exactly every five minutes are almost always the automatic save. In bukkit.yml, ticks-per.autosave controls the interval; in paper-world-defaults.yml, max-auto-save-chunks-per-tick limits how much is written per tick. A smaller value spreads the load instead of bundling it.
For small private servers, server.properties has offered the option pause-when-empty-seconds since the 1.21 series. If the value there is greater than zero, the server stops ticking the world after that time without players. On a shared root server this saves a noticeable amount of processing time.
When the JVM slows things down
If the tick time stays low on average but jumps irregularly to several hundred milliseconds, the garbage collection of the Java runtime is often to blame. You can check that directly:
spark gcmonitor
spark heapsummary
Two widespread misconceptions: more heap is not automatically better, because large heaps produce longer pauses during collection. And the heap must never be chosen so large that the operating system is pushed into swap. A swapping Minecraft server is hopelessly slow. Check this with vmstat 1 5: permanent values in the si and so columns are the death sentence. Background on this in the article Setting up swap and avoiding out of memory.
This message is a symptom, not a cause:
java.lang.OutOfMemoryError: Java heap space
Increasing the heap only postpones the crash if a plugin leaks memory or if millions of entities exist.
The distributions differ considerably when it comes to the Java version, and that is exactly where many installations fail. From Minecraft 1.20.5 onwards, Java 21 is required. Debian 12 ships only OpenJDK 17 in its standard repository and no OpenJDK 21, Debian 13 ships 21 and 25 but no 17. Ubuntu 22.04 and 24.04 have 17 and 21 in the repository. If you have to stay on Debian 12, install Temurin from the Adoptium repository. The steps are in Installing Java 21 on Debian, and for older server versions in Installing Java 17 on Debian.
Do not reach for the convenient meta package default-jre-headless. Depending on the release it points to a completely different version: Debian 13 and Ubuntu 24.04 give you Java 21, Debian 12 gives you Java 17, Debian 11 and Ubuntu 22.04 give you Java 11. The installation completes without an error everywhere, yet on the last two the server still refuses to start. So name the version explicitly, that is openjdk-21-jre-headless.
java -version
java -XX:+PrintFlagsFinal -version 2>/dev/null | grep -w MaxHeapSize
The 2>/dev/null suppresses the version banner that the JVM writes to the error output and that would otherwise sit in the middle of the result. And grep -w MaxHeapSize returns exactly one line, while a looser search would also pick up SoftMaxHeapSize.
When it goes wrong and how you get back
The server aborts with a watchdog error. The log then says, in effect, that a single tick took 60 seconds and that the server is considered crashed. This is not a malfunction but a protection mechanism against a permanently hanging process. The attached stack trace is worth its weight in gold: it shows exactly where the server was stuck. The value max-tick-time in server.properties controls this threshold. Switching it off fixes nothing, it only turns the crash into a permanently frozen server.
Things got worse after a configuration change. This is exactly why the most important rule of this whole troubleshooting applies: only one change per measurement, and write down the original value. If you touch view-distance, entity ranges and Java flags at the same time, you will not know afterwards what actually worked.
The server no longer starts after a crash. Usually level.dat is damaged. The world folder contains level.dat_old, which you can copy over it after making a copy of the broken state. For damaged region files, only the backup helps.
Everything is optimized and it still stutters. Then check the layer below: %steal in mpstat, swapping in vmstat and the disk latency. If the tick time stays high even though Spark shows no single culprit above 10%, then it is simply too much content for too little single-core performance. In that case, splitting across several instances or more processing power helps, not the next configuration tweak.
Only the players have high pings, the server is quiet. Then it is the network. A location with short paths to your player base makes the biggest difference here. For servers in Frankfurt am Main, typical latencies from German-speaking countries are in the low double digits. If outages appear suddenly and all at once, an attack can be behind it too, see Protecting servers against DDoS attacks. On the client side, messages of this kind show up:
Internal Exception: io.netty.handler.timeout.ReadTimeoutException
Timed out
Connection reset
How you can tell that it is really fixed
A fix only counts as confirmed once it holds up under exactly the load under which the problem occurred. At night with two players, even a broken server runs smoothly. Check at peak time:
spark tpsshows 20.0 in the 15-minute window and not only in the 5-second window.- The 95th percentile of the tick time is below 40 ms, the maximum below 100 ms.
- 24 hours of log without a single "Can't keep up!" line.
- A fresh Spark profile no longer shows any single item above 15%.
spark gcmonitorreports no pauses above 200 ms.- The players who reported the issue confirm the improvement at the same place and in the same situation.
Write down the before and after values, ideally with the date and the change you made each time. The next time performance drops, three months from now, this list is worth more than any guide, because it shows what has already worked on your specific server. If you are setting the server up from scratch, you will find the basics in Installing a Minecraft server on Debian and in the checklist for a new root server.
Frequently asked questions
How do I tell whether the server or my internet connection is lagging?
What does the message Can't keep up! Is the server overloaded? mean?
Do I still have to install Spark as a plugin?
Which values for view-distance and simulation-distance make sense?
Does more RAM help against low TPS?
Why does a CPU with many cores do so little for Minecraft?
Does pregenerating chunks really help against stutter?
2026 KernelHost GmbH. All rights reserved. This guide is protected by copyright. Republishing it on other websites, in whole, in part or in edited form, is not permitted without our written consent. Quoting with a source credit and a link is expressly welcome.

