New: see how many SpigotMC listing viewers end up running your plugin →

Guides · Engineering

Which part of your Minecraft plugin causes lag (and how to fix it)

Find the part of your Spigot or Paper plugin that causes lag: MSPT, spark profiles, the usual suspects and how to see its cost across all servers.

Updated · by Skilled · 4 min read

A Minecraft server has 50 milliseconds per tick to do everything: move entities, grow crops, run every plugin. When a tick takes longer, the server falls behind and players feel lag. If your plugin takes 10 ms of those 50, it's using a fifth of the server, and owners notice.

Here's how to find which part of your plugin costs the most, the patterns that usually cause it, and how to fix them.

Measure first: MSPT, not TPS

TPS (ticks per second) stays at 20 until the server is already overloaded, so it hides problems. MSPT (milliseconds per tick) shows how much of the 50 ms budget is used, long before TPS drops.

On Paper, /mspt shows it. A healthy server sits well under 50; if it's at 45 with your plugin and 25 without, you've found a suspect.

Profile with spark

spark is the standard profiler for Minecraft servers, and it ships with recent Paper builds. Run it while the lag happens:

/spark profiler start
... reproduce the lag for a minute or two ...
/spark profiler stop

You get a link to a call tree of the main thread. Open it, search for your plugin's package, and expand the heaviest branches. The method at the bottom of a thick branch is where the time goes.

Useful options:

  • --only-ticks-over 50 records only the slow ticks, so a lag spike isn't diluted by normal ticks.
  • --thread * includes other threads, if you suspect your async tasks.

spark profiles one server at a time, the one you're on. It's the right tool to fix a lag you can reproduce.

The usual suspects

Most plugin lag comes from a handful of patterns.

Heavy work in PlayerMoveEvent. It fires many times per second per player, including when they only turn their head. Bail out early when the block didn't change:

@EventHandler
public void onMove(PlayerMoveEvent e) {
    Location from = e.getFrom(), to = e.getTo();
    if (to == null || (from.getBlockX() == to.getBlockX() && from.getBlockY() == to.getBlockY() && from.getBlockZ() == to.getBlockZ())) return;
    // the expensive part, now ~20x less often
}

Loops over everything, every tick. Iterating all online players, all entities or all loaded chunks every tick adds up fast. Run it every 10 or 20 ticks if it doesn't need to be instant, or keep a set of only the things you care about.

Database and file I/O on the main thread. A MySQL query that takes 30 ms on a remote database is more than half a tick. Do I/O asynchronously and come back to the main thread with the result:

Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
    Stats stats = database.load(uuid);            // off the main thread
    Bukkit.getScheduler().runTask(plugin, () -> apply(player, stats)); // back on it
});

Loading chunks by accident. world.getBlockAt(x, y, z) or getChunkAt on an unloaded chunk loads (or generates) it synchronously. Check world.isChunkLoaded(cx, cz) first, or use Paper's async chunk API.

Updating scoreboards, holograms or boss bars every tick. Players can't read text that changes 20 times a second. Update every second, and only when the value changed.

Recomputing what could be cached. Parsing config values, building item stacks or compiling regexes on every call. Do it once on load and keep the result.

The problem with profiling one server

spark tells you what's slow on the server you're looking at. But your plugin runs on hundreds of servers with different player counts, worlds and configs, and the lag a server owner complains about may only happen with their setup. A few questions spark can't answer:

  • How much of a tick does my plugin use on average, across all servers?
  • Which of my methods costs the most in real use, not in my test world?
  • Did my last release make it slower?

That's what PluginAnalytics' performance view is for. The SDK samples the main thread about ten times a second on every server and records time spent in your plugin's code, by method, and times your event handlers and commands. The dashboard shows the milliseconds per tick your plugin uses, its most expensive methods, and a comparison between versions, so a slow release stands out and notifies you. It's the same idea as spark, aggregated across all the servers running your plugin, with nothing to install for the server owner.

Use both: the aggregate view tells you what to look at, spark on a test server tells you why.

A quick checklist

  • Measure MSPT, not TPS.
  • Profile the lag with spark while it happens, and search for your package.
  • Return early in move events; throttle per-tick loops.
  • No database or file I/O on the main thread.
  • Don't load chunks by accident.
  • Update visual things once a second, not every tick.
  • Cache what doesn't change.
  • Compare versions: make sure each release isn't slower than the last.
Skilled

Makes Minecraft plugins (including The Sift on SpigotMC) and PluginAnalytics, so other plugin developers can see how their plugins are found, installed and used.

More guides

Engineering

Error tracking for Minecraft plugins: find the bug before the 1-star review

How Spigot and Paper plugin exceptions reach the console, why waiting for reports fails, and how to collect, group and fix errors from every server.

4 min read
Engineering

How to make your Minecraft plugin Folia-compatible

Make your plugin Folia-compatible: folia-supported, the right Paper scheduler for each task, one jar for Spigot, Paper and Folia, and pitfalls.

3 min read

See how your own plugin is found and used

Free for your first plugin. One line in onEnable.