Optimising a Minecraft server

Heap sizing, Aikar's flags, view distance, Paper's entity and redstone config, and finding the plugin that is causing your lag.

Game servers9 min read

On this page
  1. Measure before you change anything
  2. Getting the numbers
  3. Heap sizing
  4. Why giving it all the RAM is wrong
  5. How to pick a number
  6. Aikar's flags
  7. What the ones that matter actually do
  8. What they will not do
  9. view-distance and simulation-distance
  10. Paper's entity and redstone config
  11. Entities
  12. Redstone
  13. Hoppers
  14. Pregenerating chunks
  15. Finding the plugin that is causing lag
  16. Related

Most Minecraft lag is not a hardware problem. On a server that is struggling, the cause is almost always view distance, the number of entities being ticked, or one or two plugins doing expensive work every tick, and none of those get better on a faster machine. Measure first, then change one thing at a time.

Measure before you change anything

Two numbers describe how the server is doing, and only one of them is useful on its own.

TPS is ticks per second and it is capped at 20. It only moves once the server is already failing to keep up, so a healthy server sits at 20 whether it has plenty of headroom or almost none.

MSPT is milliseconds per tick, which is the real measurement. The budget is 50 milliseconds. A server at 20 TPS and 45 MSPT is one bad moment away from stuttering. A server at 20 TPS and 12 MSPT has room.

Both servers report 20 TPSMilliseconds per tick (MSPT), against the 50 ms budget12 MSPT: has room12 ms45 MSPT: one bad moment from stuttering45 ms50 ms budget01020304050Milliseconds per tickBoth servers report 20 TPSMilliseconds per tick (MSPT), against the 50 msbudget12 MSPT: has room12 ms45 MSPT: one bad moment from stuttering45 ms50 ms budget01020304050Milliseconds per tick
Two servers that TPS cannot tell apart. Both report 20, but one uses 12 of its 50 ms and the other 45, which leaves it almost nothing for a bad moment. MSPT shows the difference long before TPS moves. Data: the examples on this page.

Getting the numbers

/tps on Paper gives you the tick rate and, on recent builds, the tick time.

spark is the profiler to use. Recent Paper builds include it; otherwise install it as a plugin or a mod.

  • /spark tps for tick rate and MSPT percentiles.
  • /spark health for a summary including memory and, where available, system load.
  • /spark profiler start, then wait for the problem to happen, then /spark profiler stop. It uploads a profile and prints a link showing where main-thread time went, broken down by the code that consumed it.

Older Paper builds carry the earlier /timings on and /timings paste tooling. Paper has moved to spark, so use whichever your build actually has and read the same way: find the biggest consumer of main-thread time, not the longest list.

Heap sizing

-Xms sets the starting heap and -Xmx sets the maximum. Set them to the same value. The Java runtime then does not spend time growing and shrinking the heap, and the flags below can lay the whole heap out at startup.

Why giving it all the RAM is wrong

A machine with 16 GB does not want -Xmx16G. Four separate things go wrong.

  • The Java process uses memory outside the heap. Class metadata, thread stacks, direct byte buffers used for network I/O, and the garbage collector's own bookkeeping all sit outside -Xmx. Size the heap to the whole machine and the process still needs more than that.
  • The operating system needs page cache. Region files are read and written constantly. Free memory the kernel uses to cache them is memory doing useful work. Take it all for the heap and chunk access starts hitting the disk.
  • A bigger heap takes longer to collect. Garbage collection pauses scale with how much there is to look at. A pause during a tick is a tick that overran its budget, so an oversized heap converts a memory surplus into a lag spike.
  • Overcommitting gets the server killed. If the process asks for more than the machine has, the kernel's out-of-memory killer picks the largest process, which is your server, and terminates it mid-tick.
One machine's memory, two heap sizesBar length is a share of the machine, not a measurement-Xmx set to the whole machineOverHeap sized below the machineFitsThe machine's memoryHeap (-Xmx)Outside the heapOutside the heap, with nowhere to goPage cacheOS and anything else on the boxOne machine's memory, two heap sizesBar length is a share of the machine, not ameasurement-Xmx set to the whole machineOverHeap sized below the machineFitsThe machine's memoryHeap (-Xmx)Outside the heapOutside the heap, with nowhere to goPage cacheOS and anything else on the box
Set the heap to the whole machine and the heap alone fills it, so the memory Java uses outside the heap has nowhere to go and the page cache is gone. Sized below the machine, the heap leaves room for both and for everything else on the box. Illustration.

How to pick a number

Start below what you think you need and let the measurement move it.

  1. Leave the operating system and anything else on the box a comfortable share of the machine's memory. If you also run a database, a proxy, or a second game server, count those first.
  2. Run the server under load and watch heap usage with /spark health or the memory view in a spark profile.
  3. If used heap sits pinned near the maximum and the profile shows significant time in garbage collection, raise -Xmx.
  4. If used heap is comfortably below the maximum, more heap changes nothing. Extra headroom is not extra performance.

Aikar's flags

The default garbage collector settings are tuned for throughput. A Minecraft server wants the opposite: short, predictable pauses, because any pause longer than the remainder of the tick budget is visible to players. The widely used answer is the flag set published as Aikar's flags.

Shell
java -Xms4G -Xmx4G \
  -XX:+UseG1GC -XX:+ParallelRefProcEnabled -XX:MaxGCPauseMillis=200 \
  -XX:+UnlockExperimentalVMOptions -XX:+DisableExplicitGC -XX:+AlwaysPreTouch \
  -XX:G1NewSizePercent=30 -XX:G1MaxNewSizePercent=40 -XX:G1HeapRegionSize=8M \
  -XX:G1ReservePercent=20 -XX:G1HeapWastePercent=5 -XX:G1MixedGCCountTarget=4 \
  -XX:InitiatingHeapOccupancyPercent=15 -XX:G1MixedGCLiveThresholdPercent=90 \
  -XX:G1RSetUpdatingPauseTimePercent=5 -XX:SurvivorRatio=32 \
  -XX:+PerfDisableSharedMem -XX:MaxTenuringThreshold=1 \
  -jar paper.jar --nogui

What the ones that matter actually do

  • -XX:+UseG1GC selects the G1 collector, which works in bounded increments instead of stopping everything to do a full pass.
  • -XX:MaxGCPauseMillis=200 is the pause target G1 aims for when deciding how much to collect at once.
  • -XX:G1NewSizePercent and -XX:G1MaxNewSizePercent enlarge the young generation. Minecraft produces a great deal of very short-lived garbage, and collecting it while it is still young is much cheaper than letting it be promoted first.
  • -XX:+AlwaysPreTouch faults in the whole heap at startup, so pages are committed before players arrive rather than during a tick. Startup takes slightly longer in exchange.
  • -XX:+DisableExplicitGC ignores a plugin calling for a full collection. One badly behaved plugin can otherwise stall the entire server on demand.
  • -XX:InitiatingHeapOccupancyPercent=15 starts concurrent marking early, so G1 keeps ahead of allocation instead of falling back to a full collection.

What they will not do

These flags move pauses around. They do not give the server more throughput, and they will not rescue a plugin that ticks four thousand entities every tick. If your profile shows the time going to plugin code or entity ticking, garbage collector flags are not the fix.

The set above is for a moderate heap. At larger heaps the published flags change: the young generation share and the heap region size both go up. Rather than editing the numbers by hand, generate the set for your heap size and Java version with the flags.sh generator, which is the canonical source for this flag set.

view-distance and simulation-distance

These two settings in server.properties have more effect on tick time than anything else you can change without touching gameplay.

view-distance is how many chunks out from a player the server sends to their client. The cost is roughly the square of the radius and it applies per player, so dropping it from 10 to 8 removes far more work than the difference sounds like. It affects how far players can see.

simulation-distance is how far out the server actually ticks the world: entities, redstone, block updates, mob spawning, and crop growth. This is the one that burns main-thread time. Chunks between the simulation distance and the view distance are still sent to the client, but nothing in them moves.

Chunks around one playersimulation-distanceTicked: entities, redstone, block updates, mobspawning, crop growth. This is what burnsmain-thread time.view-distanceSent to the client so the player can see it.Nothing in the outer ring moves.view-distance costs roughly the square of itsradius, for every player online.Chunks around one playersimulation-distanceTicked: entities, redstone, block updates, mobspawning, crop growth. This is what burnsmain-thread time.view-distanceSent to the client so the player can see it.Nothing in the outer ring moves.view-distance costs roughly the square of itsradius, for every player online.
The ticked square sits inside the sent one. Everything in the inner square costs main-thread time every tick; the outer ring only has to be sent, which is why lowering simulation-distance first saves the most for what players notice. Illustration.

Practical order of attack:

  1. Lower simulation-distance first. It is the expensive one, and players notice it far less than a shorter view.
  2. Keep simulation-distance at or below view-distance. Setting it higher simulates chunks nobody can see.
  3. Change it by 2, restart, and compare MSPT under the same player load. Guessing at a value without measuring is how you end up with a server that looks worse and runs the same.

Two related settings in the same file:

  • entity-broadcast-range-percentage scales how far away entities are sent to clients. Lowering it cuts outbound update traffic and client-side load on busy servers.
  • network-compression-threshold is the packet size above which the server compresses. Raising it trades bandwidth for CPU, lowering it does the reverse. Leave it alone unless you have measured a reason.

Paper's entity and redstone config

Paper exposes a large amount of behaviour that vanilla hardcodes. Since Paper 1.19 the files are config/paper-global.yml and config/paper-world-defaults.yml, with per-world overrides in <world>/paper-world.yml. Older builds used a single paper.yml. Alongside them sit spigot.yml and bukkit.yml, which Paper still reads.

Option names and locations move between Paper versions, so find yours rather than trusting a path from a guide. From the server directory:

Shell
grep -rn "redstone-implementation" config/ *.yml

Entities

Entities are the usual cause of a slow tick on an established server.

  • entity-activation-range in spigot.yml sets how close a player must be before each category of entity is ticked at all. Distant mobs stand still instead of consuming time. Lowering the ranges for monsters and animals is the single biggest entity win, at the cost of farms further away from a player running more slowly.
  • merge-radius in spigot.yml combines dropped items and experience orbs that land near each other, so a mob farm produces one stack rather than hundreds of separate entities.
  • spawn-limits and ticks-per in bukkit.yml control how many mobs of each category can exist per player and how often the spawning routine runs. This is the lever for a server that is full of mobs.
  • Despawn ranges in the Paper world config decide how far from a player a mob has to be before it is removed. Tightening them keeps the entity count down on a busy map.

Redstone

The Paper world config carries a misc.redstone-implementation option with alternatives to the vanilla algorithm. The alternatives compute the same dust power levels with far less work, which matters enormously on a server with large contraptions, and they change behaviour in a few edge cases that some machines depend on. Switch it, then test your players' existing builds before you leave it that way. If the option is not in your config file, your build predates it.

Hoppers

The Paper world config has a hopper section, including an option to skip firing the inventory move event for every hopper transfer. On a server with a lot of hoppers this is a large saving.

Pregenerating chunks

Generating new terrain is the most expensive single thing a Minecraft server does, and by default it happens whenever a player walks somewhere new. That is the classic symptom of the whole server hitching when one person explores.

Pregenerating moves that work to a time you choose.

OverworldNether0, 0/chunky radius 5000From the centre to the edge./worldborder set 10000A diameter: edge to edge, twice the radius.Nether radius 625An eighth of 5000, because the Nether iscompressed eight to one.
The numbers in the steps below, drawn to scale; the markers match steps 2 to 4. Chunky takes a radius and the world border a diameter, so a 5000 radius needs a 10000 border, and the Nether covers the same ground with an eighth of the radius. Derived from the commands on this page; the Nether radius of 625 is an eighth of 5000.
  1. Decide the size first

    A pregenerated world is a fixed cost on disk, and terrain that goes on forever means a world folder that goes on forever. Pick a radius your players can live with and check you have the space with df -h before you start.

  2. Run the generation

    Chunky is the usual tool, and exists as a plugin for Paper and as a mod for Fabric and Forge. From the console:

    /chunky world world
    /chunky center 0 0
    /chunky radius 5000
    /chunky start

    Expect high CPU and heavy disk writes for as long as it runs. Do it before the server opens, or during your quietest hours. It can be paused with /chunky pause and picked up later with /chunky continue.

  3. Set a world border to match

    Without a border, players walk straight past the pregenerated area and you are back where you started. The vanilla command takes a diameter, so it is twice your Chunky radius:

    /worldborder center 0 0
    /worldborder set 10000
  4. Repeat for the Nether and the End

    They are separate worlds and are generated separately. The Nether is compressed eight to one against the overworld, so its radius is an eighth of the overworld's for the same coverage.

Finding the plugin that is causing lag

Be honest with yourself about this step. Plugins and view distance cause most Minecraft lag. A faster machine raises the ceiling, and a plugin that does expensive work on every tick will rise to consume whatever ceiling you give it.

  1. Profile while the problem is happening

    /spark profiler start, wait for the symptom, /spark profiler stop. A profile of a healthy period tells you nothing.

  2. Read it top down

    The viewer attributes main-thread time by the code that used it, and plugin code carries the plugin's own package name. A plugin at the top of the list, or a scheduled task running every tick, is your answer.

  3. Separate the categories

    If the time is not in plugin code, it will be somewhere identifiable: entity ticking, chunk loading and generation, or world saving. Entity ticking sends you to the entity config above, chunk generation sends you to pregeneration, and world saving usually means the disk.

  4. Confirm by removing it

    Stop the server, move the suspect jar out of plugins/, start, and measure again under similar load. This is the only step that proves it. See Installing and managing Minecraft plugins for doing that on a copy rather than on the live server.

Need a server, or a hand with one?

Pick a plan to deploy a server, or open a ticket in the panel. Support runs 24/7/365.