Lag on a Minecraft server is a budget problem. Every tick has fifty milliseconds to simulate the whole world, and when the work does not fit, the tick runs long and the clock slips. spark exists to tell you where those fifty milliseconds went, by name: this plugin, this method, this entity type, this chunk load. Ten minutes with a profiler beats a week of removing plugins one at a time, and it ends the argument with a link instead of an opinion. What follows is how to take a report that is worth reading, and how to read it.
MSPT is the number that matters, not TPS#
TPS - ticks per second - is the number everybody quotes, and it is the less useful of the two. It is capped at 20, because the server will never run faster than the game's clock. MSPT, milliseconds per tick, is the actual measurement: how long the work in a tick took. The relationship is simple. As long as MSPT stays under 50, TPS reads 20.0. Once MSPT crosses 50, TPS falls in proportion.
That cap is why TPS hides the interesting part of the story. A server at 20.0 TPS with an average MSPT of 45 is not healthy; it is one large farm, one new plugin or one busy Saturday away from slipping, and it will feel worse than the number suggests because the spikes are already over 50 even if the average is not.
| Reading | What it means |
|---|---|
| TPS 20.0, MSPT 12 | Healthy. Plenty of headroom |
| TPS 20.0, MSPT 45 | Full speed with nothing to spare |
| TPS 18.5, MSPT 54 | Ticks are overrunning. About 8% slower than real time |
| TPS 20.0 average, MSPT max 300 | Spikes. The average is lying to you |
Averages are the second trap. Players do not notice a mean, they notice the moment the server stops for a third of a second while a chunk loads on the main thread. So look at the maximum and the distribution, not just the average, and treat a report as two separate questions: is the server steadily over budget, or is it fine except for spikes? The answer changes which tool you reach for. What tick rate actually means has the longer version of why a fixed tick makes this so unforgiving.
One more distinction worth having early. Server-side lag makes everyone stutter at once, blocks break and reappear, and mobs teleport. Client-side lag, or a bad route between one player and the server, affects one person while everyone else is fine. spark measures the server. If your TPS is 20.0 and MSPT is 15 and somebody still complains, you are looking in the wrong place - go and read latency, jitter and packet loss instead.
Installing spark and the three commands to run before you profile#
spark is a single jar in plugins/ on Paper and Spigot, and a mod in mods/ on Fabric, Forge and NeoForge. It also runs on Velocity and BungeeCord, where it profiles the proxy rather than a backend. Some server software now ships it already, so before you upload anything, type /spark in the console. If you get a help message back, it is there.
Commands need op or the spark permission, and the console always has it. Sampling is asynchronous and cheap - low single-digit percent of a core on a normal server - but it is not free, and it is not something to leave running for a week.
Before taking a profile, get a picture of the shape of the problem:
$ spark tps$ spark health --memory$ spark tickmonitor --threshold 200Each of those answers a different question.
/spark tpsprints TPS over several windows (5 seconds through 15 minutes), MSPT for the last 10 seconds and the last minute, and - the part people miss - CPU usage for both the system and this server's process. On a container with a hard CPU limit, a process pinned at its share is the whole diagnosis: the server is not waiting on a slow plugin, it is waiting for a core. A server at 100% of its share is slow, not broken./spark health --memoryadds heap usage, garbage collection statistics and disk space. It answers "is this a memory problem?" in one screen./spark tickmonitor --threshold 200prints a line for every tick that takes more than twice the average. Leave it running for a few minutes and you learn whether the spikes are regular (a scheduled task, an autosave) or tied to something a player did.
Those three answers together decide the profile you take next. Steady overspend wants a long, plain profile. Spikes want a filtered one.
Taking a profile that is worth reading#
The default is fine for a steady problem:
$ spark profiler start --timeout 300That samples the main server thread for five minutes and then stops on its own, which is better than starting one and forgetting. Five minutes with players online beats thirty seconds on an empty server; the report can only contain work that happened while it was running, so profile the server in the state people complain about.
The flags that change the answer:
| Flag | What it does |
|---|---|
--timeout <seconds> | Stop automatically after this long |
--thread <name> | Profile a named thread instead of the main one |
--thread * | Profile every thread |
--only-ticks-over <ms> | Record only ticks longer than this |
--interval <ms> | Sampling interval, default about 4 ms |
--alloc | Profile memory allocation instead of CPU time |
--only-ticks-over is the one that earns its keep. A report built from every tick is dominated by the ordinary work the server does 20 times a second, and the 300 ms freeze you actually care about is 0.5% of the samples. Run it again as /spark profiler start --only-ticks-over 100 --timeout 600 and the report contains nothing but the bad ticks, so whatever caused them is suddenly at the top.
Use --thread * when the main thread looks innocent: chunk generation, chunk saving and plugin async tasks all live elsewhere, and a plugin running a "background" job that hammers the disk will show up nowhere else. Use --alloc when memory climbs fast and garbage collection is the visible symptom, because that report names the code creating the objects rather than the code using the CPU.
When the profile stops, spark uploads it and prints a link to its web viewer. Run /spark profiler with no arguments to see whether your build supports saving to a file instead - worth knowing if the plugin names and world names in a report are something you would rather not publish. The link is unlisted rather than secret.
Reading the report: sources, flat and the call tree#
The viewer opens on a call tree, which is the least useful view to start with. Switch views in this order.
Sources attributes time to the plugin or mod it came from. This is the view that answers the question you actually have. It merges everything a plugin does - its event handlers, its scheduled tasks, the work it triggers inside the server - into one percentage. If one plugin has 35% of the main thread, you are finished; go and read its configuration. If the top entry is "Minecraft" or the server itself, no plugin is at fault and the world is the problem.
Flat sorts methods by self time: how long was spent inside that method rather than in things it called. This finds the specific hot loop. It is where you learn that the cost is not "entities" in general but pathfinding, or not "the shop plugin" but one database query.
All is the full tree, and it is worth opening once you have a suspect, because it shows the route the work took to get there. A plugin's listener sitting under a vanilla method tells you the plugin is reacting to something the world does often, which is a different fix from a plugin doing its own thing on a timer.
Two habits make the reports readable. First, modern Paper runs with Mojang's own mappings, so classes appear as ServerLevel and HopperBlockEntity rather than the obfuscated aab.a() you get on older Spigot builds - if your report is a wall of two-letter class names, that is why. Second, percentages in the report are percentages of the profiled window, not of the tick budget. Forty percent of a thread that was only 30% busy is not an emergency.
The five things a report usually names#
Nearly every report on a normal survival server lands on one of these.
- Entity ticking. Look for
ServerLevel.tickNonPassengerwith mob classes and goal or navigation methods underneath. This is a mob farm, a chunk full of villagers, or several hundred items on the floor. Pathfinding is the expensive part, and it gets worse in confined spaces where the mob cannot find a route. - Chunk loading on the main thread. Terrain that has never been generated is generated when somebody walks into it, and although Paper moves much of that work off the tick, the load and the lighting still cost. The signature is spikes that follow a player rather than a steady cost, often while somebody is flying in an elytra or a new nether portal links. On recent Paper,
/paper syncloadinfonames what is forcing a chunk to load synchronously, which is frequently a plugin. - Block entities. Hoppers tick whether or not anything moves, and a wall of them with nothing in them still costs.
HopperBlockEntitynear the top of the flat view means a storage system needs redesigning or throttling. - Redstone. Large clocks and update-heavy contraptions show up as neighbour-update and wire methods. A single player's farm can own a tenth of the budget.
- Blocking work inside a plugin. The worst finding and the easiest to fix. A socket read, a file read or a web request sitting under a plugin's event handler means the whole server stopped while that call waited. Economy and stats plugins doing a query on join are the classic. Nothing in the game is wrong; the plugin is.
If the report names a plugin you cannot change, you still gain something: a report link is exactly what an author needs to fix it, and it is far more persuasive than "your plugin is laggy".
Memory, garbage collection and heap summaries#
Memory problems look like lag but are not caused by slow code. The tell is regular spikes with no obvious source, plus garbage collection time in the health report.
$ spark health --memory$ spark gcmonitor$ spark heapsummary/spark gcmonitor prints each collection as it happens. Short young-generation pauses, single-digit to low tens of milliseconds, are normal and unavoidable. Repeated long pauses, or a heap that sits near its ceiling and never comes down after a collection, mean the heap is too small for the world or something is holding references it should not. /spark heapsummary lists live objects by class with counts, which is how you discover that the real problem is 400,000 dropped items or a plugin accumulating one object per block placed since the last restart.
The counter-intuitive part: a bigger heap is not automatically better. A very large heap makes each collection longer, and it hides a leak instead of exposing it. Size the heap to the world and the plugin list, set the minimum equal to the maximum, and leave headroom outside the heap for the JVM itself - Minecraft JVM flags and Java versions covers the numbers, and how much RAM a Minecraft server needs covers the sizing. Remember that a container's memory limit applies to the whole process, not just the heap, so -Xmx equal to your plan's memory is how servers get stopped.
Finding the chunk or the entity behind the numbers#
A report says "entity ticking is 30% of your tick". It does not say "at x 1180, z -2044". Paper's own commands close that gap:
$ paper entity list$ paper mobcaps$ minecraft:debug start$ minecraft:debug stop/paper entity list prints entity counts grouped by type with chunk coordinates, so you can sort by the worst chunk and go and look. The exact syntax and filters vary by Paper version, so run it without arguments first. /paper mobcaps shows how close each spawn category is to its limit, which tells you whether a farm is holding the natural spawn cap hostage for the whole server.
Vanilla's own profiler is still there too. /minecraft:debug start and /minecraft:debug stop write a report into the debug folder with per-tick timings broken down by the game's internal sections, and it is the better tool for datapack and command-heavy servers because it attributes time to functions. Note the minecraft: prefix: on a Bukkit-based server, plain /debug may resolve to something else entirely.
Once you know the chunk, the fix is usually physical. Cap the farm, kill the item backlog, tighten the hopper chain, or ask the player to rebuild it. Removing entities with a blunt clear-all command works and annoys people, so tell them first.
What to change, and in what order#
Cheapest first, and measure after each one rather than changing five things and declaring victory.
- Clear the backlog you found and cap what created it. An items-on-floor problem is a design problem, not a hosting problem.
- Drop
view-distanceand thensimulation-distanceby one or two inserver.properties. Nobody notices; the server does. This is the single highest-value change on most small servers and is covered in detail in the Paper optimisation guide. - Fix or remove the plugin the report named. Configuration first - most heavy plugins have a scan interval or a radius you can reduce - removal second.
- Pre-generate the world and set a border so exploration stops generating terrain during play. See world border and pregeneration.
- Add a restart schedule if, and only if, memory or MSPT climbs over days rather than minutes. Restart schedules that help explains the difference between a useful restart and a superstition.
- Only then buy more CPU. It is the most expensive fix and the one least likely to help if the answer was a plugin, because the game's main loop is single-threaded and does not spread across cores.
Take a second profile after the change, in the same conditions, and compare. Two reports are a result. One report plus a feeling is a story. Why TPS drops and what to do ranks the same fixes by what they cost you in gameplay.
FAQ#
Does spark itself cause lag?
Barely. Sampling is asynchronous and the overhead is a few percent of one core while a profile runs, which is smaller than the thing you are hunting. Leaving a profile running indefinitely is still a bad habit: stop it, or start it with --timeout.
My TPS is 20 but the server still feels bad. What now?
Look at maximum MSPT rather than the average, and run /spark tickmonitor. A server that averages 20 ms and spikes to 400 ms twice a minute reads as perfect and plays badly. If MSPT is genuinely flat and low, the problem is the network path or the client, not the server.
Is spark better than Timings?
It answers more questions. Timings told you which handlers were expensive; spark profiles the actual JVM, so it also sees garbage collection, blocking calls, work on other threads and vanilla internals. Paper's own guidance moved to spark, and Timings is no longer the recommended tool.
What is a normal MSPT for a small survival server?
With a handful of players, a pre-generated world and a short plugin list, 5 to 20 ms is typical. Thirty to forty is a busy server that still works. Anything consistently over 50 is running behind real time and players can feel it.
Can I profile a proxy network?
Yes, but profile the right process. spark on Velocity or BungeeCord measures the proxy, which is almost never the cause of tick lag because it does not simulate a world. Profile the backend the players were on when it stuttered. Velocity proxy networks covers how the pieces fit.
Why does memory keep climbing even when TPS is fine?
Up to a point that is normal: more loaded chunks and more explored world means more to hold. What is not normal is a heap that never drops after a collection. Run /spark heapsummary and look at which class has an implausible number of instances.




Comments
Completely anonymous: no account, no email, no cookie. We store the name you type, the text and the time - nothing else. Links are limited and markup is not rendered.