Your Minecraft Server Isn't Slow, It's Misconfigured
Can't keep up! is not a hardware message. It's the server telling you a tick took longer than 50ms, and almost every cause is a default nobody changed. Here's what actually moves MSPT, with the flags that are now wrong and the setting that isn't where every guide says it is.
Can't keep up! Is the server overloaded?
Eight players. One farm. That message every few seconds, TPS at 12, blocks reappearing after you break them, mobs frozen mid-stride, and someone in chat asking if you're going to fix it.
You're probably about to buy more RAM. Don't. That message doesn't mean your hardware is inadequate — it means a single tick took longer than 50 milliseconds, and the overwhelming majority of the time that's a default nobody changed. Mojang ships settings tuned for one person in a creative world. You're running twenty people, four farms and a villager hall, and nothing in that config knows it.
Here's what actually moves the number, in the order it's worth doing.
Buy the Right CPU, Not the Most RAM
Before any of it, the hardware conversation, because people get this backwards and then spend money fixing the wrong thing.
Minecraft is a single-thread problem. The tick loop runs on one core. Paper moved chunk I/O and some other work off it, but the thing that determines whether you keep up — entity AI, redstone, block updates — is one thread on one core. Which means single-core performance is the spec that matters, and a 32-core server CPU with mediocre per-core clocks will lose to a 6-core desktop chip with fast ones.
Check single-thread benchmark rankings before you buy, not core counts. This is also why "just get more RAM" almost never fixes lag: RAM affects how often you collect garbage, not how fast one thread processes three thousand entities.
Never run a server off a spinning disk. Minecraft is I/O-heavy, and it gets heavier with view distance and player count. An SSD isn't an upgrade here, it's the floor.
Be careful with shared hosting. Shared hosts sell you two things: guaranteed resources, which are usually too low to run anything, and shared resources, which you only get when nobody else on the box wants them. They oversell deliberately — the same model as airline seats — so the moment the machine gets busy, every server on it degrades together. You will spend a weekend tuning configs to fix a problem that is somebody else's server on the same host.
Stop Running Vanilla, and Understand Why
The server JAR is the highest-leverage decision you make, and it's the one people skip because vanilla is what Mojang hands you.
Vanilla ticks every loaded entity, every tick, regardless of whether a player is within a thousand blocks of it. It has no entity activation ranges. Its chunk I/O is synchronous. It was built to be correct and comprehensible, not to survive twenty people and a gold farm.
Paper is the answer for almost everyone. Async chunk I/O, entity activation ranges, and a long list of behavioural optimisations, with full Spigot/Bukkit plugin compatibility. There is no meaningful downside and it is free.
Purpur is a Paper fork that exposes more configuration. If Paper doesn't have the knob you want, Purpur probably does. Plugin-compatible with Paper.
Pufferfish and Leaf target higher player counts with additional tuning. Worth evaluating if you're past 100 players and Paper is genuinely the ceiling — measure first.
Folia is a different thing entirely, and worth being blunt about: it removes the main thread and ticks independent regions in parallel. The ceiling is enormous. The cost is that every plugin you own needs updating, because BukkitScheduler is deprecated wholesale and Folia refuses to load plugins that haven't opted in. It also only helps when players are spread out — forty people standing at spawn is one region, which is one thread, which is Paper with extra overhead.
For a normal server in 2026: run Paper.
Java 21 or 26, and the Flag That Will Bite You
Modern Minecraft needs Java 21 minimum. Java 26 brings real GC improvements. Both are defensible — 21 is LTS and boring, 26 is faster and isn't.
But if you're on 26, there is a flag in circulation that you must not copy.
Nearly every "high player count" guide tells you to switch to ZGC like this:
-XX:+UseZGC -XX:+ZGenerational
That was correct on Java 21, where generational ZGC existed but was off by default. You can confirm it on a 21 JVM:
$ java -XX:+UseZGC -XX:+PrintFlagsFinal -version | grep ZGenerational
ZGenerational = false
Then the ground moved. JEP 474 made generational mode the default in JDK 23 and deprecated the flag. JEP 490 removed non-generational ZGC entirely in JDK 24 and obsoleted the option — and states plainly that it "will expire in a future release, at which point it will not be recognized by the HotSpot JVM, which will refuse to start."
So on Java 26 that flag does nothing except print an obsolete-option warning, and on some future JDK it will stop your server from booting. If you want ZGC on a modern JVM, the whole flag is:
-XX:+UseZGC
That's it. Generational is already what you get.
Aikar's Flags, and What Each One Is Buying
The default JVM configuration is wrong for Minecraft in a specific way. The game allocates enormous numbers of small, short-lived objects — chunk sections, entity state, packet buffers — at very high rates. Default G1 tuning promotes too many of them into old generation, and collecting old generation is expensive and slow.
Aikar's Flags exist to fix that allocation profile. PaperMC recommends them and they've been the standard for years:
java -Xms10G -Xmx10G \
-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
Four of those are doing most of the work.
-Xms equal to -Xmx. The JVM grows the heap by requesting memory and, on some paths, collecting to justify the expansion. Fixing both ends removes that entirely — you pay once at startup and never renegotiate. On a 16GB box allocate 12–13G, not 16.
-XX:+AlwaysPreTouch. Touches every heap page at startup so the OS actually commits it, instead of taking page faults during play. It makes your startup slower and your gameplay smoother, which is the correct trade for a server.
G1NewSizePercent=30 and G1MaxNewSizePercent=40. This is the pair that matters most. It hands the young generation a much larger share of the heap, giving short-lived objects room to die where collection is cheap. Without it, Minecraft's allocation rate pushes objects into old generation, and old-generation collections are what your players feel as rubberbanding.
MaxTenuringThreshold=1. Promote after one survival instead of fifteen. Minecraft objects are overwhelmingly either instantly dead or effectively permanent; the middle ground the survivor space exists to serve barely exists here.
-XX:G1HeapRegionSize=8M deserves more than a footnote, because it's the flag whose absence does the most damage. G1 auto-calculates region size, and PaperMC's own documentation is blunt about what happens when you let it: any allocation at or above half a region is treated as humongous and promoted straight to old generation, where it's much harder to free. Let Java pick a small region size on a modest heap and, in their words, you will be destroyed with a significant chunk of your memory getting treated as humongous.
That's the same mechanism that bites JVM services generally — object size colliding with region size — and it's why this flag is pinned rather than left to ergonomics.
Above 12GB of heap, five flags change, not one. This is the part most reposts of Aikar's flags drop:
-XX:G1NewSizePercent=40
-XX:G1MaxNewSizePercent=50
-XX:G1HeapRegionSize=16M
-XX:G1ReservePercent=15
-XX:InitiatingHeapOccupancyPercent=20
Swapping only the region size to 16M and leaving the 30/40 new-generation ratios gives you a combination Aikar never tuned. Change all five or change none.
And Aikar's own warning on that set, worth repeating verbatim in spirit: if you see an increase in old-generation collections after switching, go back to the base flags. The large-heap tuning is not strictly better, it's differently tuned.
On sizing generally, PaperMC recommends reducing Xmx/Xms by roughly 1000–1500M below your available memory. Java needs allocation beyond the heap, and a container that hits its limit gets the OOM killer rather than a graceful OutOfMemoryError.
And -XX:MaxGCPauseMillis=200 is a target, not a promise. G1 will miss it. Don't drive it below 100 chasing lower numbers — G1 responds by collecting more often, and you trade a few milliseconds of pause for a real loss in throughput.
Your GC Log Is Under-Reporting the Stall
Worth knowing before you tune anything, because it changes how you read every number that follows.
The pause duration in a GC log is not how long your server was stopped. A stop-the-world pause is a safepoint with four phases: getting every thread to stop, cleanup, the collection itself, and resuming. The GC log reports the third one.
Turn on both logs and compare the same event:
-Xlog:gc -Xlog:safepoint
The safepoint record gives you Reaching safepoint, At safepoint, Leaving safepoint and a Total. On a busy JVM those totals routinely exceed what the GC log reported — sometimes by a little, occasionally by an order of magnitude, when threads are slow to stop or slow to resume.
Which means: if your GC log looks clean but players are still complaining, you are reading the wrong log. And not every safepoint is a GC. Deoptimisation, class loading and a handful of other operations stop the world too, and no amount of G1 tuning touches them.
The Config Files Are Half Your Performance
Flags get you a well-behaved heap. Configuration gets you less work per tick.
server.properties:
# Chunks SENT to the client. Default 10.
view-distance=7
# Chunks the server actually TICKS. Far more expensive.
# Set this deliberately low -- view-distance covers what players see.
simulation-distance=4
# Packet size before the server compresses it.
# 256 is the default. See the note below before changing it.
network-compression-threshold=256
enable-rcon=false
The view-distance versus simulation-distance split is the one people miss, and it's the cheapest win in the whole file. View distance is what gets sent to the client. Simulation distance is what the server actually ticks — furnaces smelting, crops growing, mobs thinking. You want simulation set deliberately low, because view distance already covers what players can see. They get a generous horizon; the server only pays for a small core of it.
One behaviour to know: the total distance sent to the client is the greater of the two. Set simulation to 4 and view to 12 and clients receive 12 chunks. Setting simulation higher than view doesn't buy you anything except work.
network-compression-threshold is worth understanding rather than copying. It's the packet size above which the server compresses before sending — trading CPU for bandwidth. The default of 256 is right for players connecting over the open internet.
But if your server sits behind a proxy on the same machine or the same LAN, with sub-2ms latency, set it to -1 and disable compression entirely. You're burning CPU to shrink packets travelling over a link that was never the bottleneck. This is exactly what large networks do — the published Folia stress tests ran with compression off for precisely this reason, because a Velocity proxy was handling the player-facing side.
Don't do this on a server players connect to directly. Uncompressed traffic to someone on a bad connection is worse than the CPU you saved.
You may notice sync-chunk-writes missing here. Most guides tell you to set it false. On Paper and its forks that line does nothing — Paper forces it false regardless of what your properties file says. It's only worth setting on vanilla, Fabric or Spigot.
spigot.yml — entity activation range is the single highest-value block in any config file:
world-settings:
default:
entity-activation-range:
animals: 16
monsters: 24
raiders: 48
misc: 8
water: 8
villagers: 16
flying-monsters: 48
merge-radius:
item: 3.5
exp: 4.0
ticks-per:
hopper-transfer: 8
hopper-check: 8
Activation range tells the server to stop running AI, pathfinding and physics for entities with no player nearby. Without it, every mob in every loaded chunk runs a full AI cycle twenty times a second forever. This one block routinely recovers a large share of tick time on a survival server, and the cost is that mobs beyond the range stand still until someone approaches — which is exactly what you want.
merge-radius combines nearby dropped items and XP into single entities. On any server with farms this is enormous, because a farm's real output is not items, it's entity count.
Hoppers deserve their own mention. Every hopper checks for items above it on a schedule, and a redstone-heavy server has thousands of them. Raising hopper-transfer and hopper-check to 8 makes hoppers move items every 8 ticks instead of every tick. Players building item sorters will notice. Your MSPT will notice more.
The Setting That Stops a Crash, Not a Lag Spike
Everything so far is about tick time. This one is about your server refusing to start.
entities:
spawning:
entity-per-chunk-save-limit:
experience_orb: 16
arrow: 16
spectral_arrow: 16
trident: 16
snowball: 8
egg: 8
ender_pearl: 8
firework_rocket: 8
area_effect_cloud: 8
wither_skull: 4
experience_bottle: 3
This caps how many entities of each type get written to disk per chunk. Without limits, a skeleton farm running unattended, or a player emptying a shulker of fireworks into a chunk, can accumulate tens of thousands of projectiles in one region file. The server saves all of them. Then it tries to load them again on restart, and the chunk load either takes minutes or the server dies outright.
Projectiles are the priority because they're what accumulates fastest and matters least. Nobody misses the twelve thousand arrows in a skeleton grinder.
Worth being precise about the scope: this is a save limit, not a spawn limit. It won't stop a player building a huge mob farm and it isn't designed to. It stops the farm's debris turning a region file into something your server can't reopen.
And while you're in the spawner settings:
# spigot.yml
nerf-spawner-mobs: true
Mobs from monster spawners get no AI at all. They stand there, which is exactly what you want from a grinder — they exist to be killed, not to path. If your players build spawner farms, this is free performance. If they don't, it changes nothing.
Three Paper Settings Worth More Than Most Guides Admit
These live in the Paper world config and each one targets a specific, common source of tick time.
Swap the redstone engine.
misc:
redstone-implementation: ALTERNATE_CURRENT
Vanilla redstone generates enormous numbers of redundant block updates. ALTERNATE_CURRENT is a reimplementation that produces the same results with far less recalculation. It can introduce minor differences on extremely technical contraptions, and for the overwhelming majority of servers the trade is not close — if you have players doing redstone at all, this is the single highest-value line in the file.
Lobotomise the villagers that are already stuck.
entities:
behavior:
village:
lobotomize:
enabled: true
Villager AI is among the most expensive computations in the game — constant pathfinding to beds, workstations and each other. A villager sealed in a 1×1 trading-hall cell is burning that cost every tick to path somewhere it can never reach. Lobotomising strips the AI from villagers that can't pathfind to their destination; free one and it recovers.
The caveat matters and the reference states it plainly: only turn this on if villagers are actually causing lag, because the pathfinding check itself costs something. Measure first.
Stop armour stands doing collision lookups.
entities:
armor-stands:
do-collision-entity-lookups: false
Armour stands participating in collision checks is the mechanism behind a whole class of deliberate lag machine — stack a few thousand in one spot and the collision maths cripples the server. Turning it off removes the attack surface and costs almost nothing legitimate.
The Setting That Isn't in server.properties
Here's a correction worth stating loudly, because a large share of the guides on the internet get this wrong.
Entity cramming is a gamerule, not a server property. Putting max-entity-cramming=24 in server.properties does nothing at all. The file will accept the line, the server will ignore it, and you will believe you've capped mob density when you haven't.
The actual command:
/gamerule maxEntityCramming 24
Default is 24. Entities beyond that limit in a single block start taking suffocation damage, which naturally caps the density of an AFK farm. Set it lower to be more aggressive; set it to 0 to disable cramming entirely, which you'd only want on a trading hall where you need dense villagers.
One interaction to know: Paper's max-entity-collisions limits how many collision checks an entity performs, and setting it below your cramming value breaks the gamerule, because cramming damage is driven by collision counting. If you tune one, check the other.
The Setting That Can Hang Your Server Outright
Treasure maps are the sharpest edge in the whole config, and almost nobody knows about them until it happens.
When a map is generated — from a villager trade, a buried treasure chest, a loot table — the server has to locate the structure it points at. If that structure sits in an ungenerated chunk, the server generates terrain synchronously, on the main thread, searching outward until it finds one. On an unexplored world with a distant structure, that is not a lag spike. That's a hang.
# Paper world config
environment:
treasure-maps:
enabled: false
Turning them off entirely is the safe default. It is only safe to leave them on if you have pre-generated your world and set a vanilla world border, so the search can't wander into ungenerated terrain.
If you want to keep them, at minimum change what they're allowed to point at:
environment:
treasure-maps:
find-already-discovered:
loot-tables: true
villager-trade: true
The default forces newly generated maps to find an unexplored structure — which is, by definition, likely to be in a chunk that doesn't exist yet. Setting these to true lets maps point at structures already discovered, and that single change is the difference between a map trade being free and a map trade freezing your server while it generates terrain looking for a shipwreck.
Note the ordering dependency: this is a second reason to pre-generate, and it's the one that turns a performance nicety into a stability requirement.
Two smaller ones while you're in there:
tick-rates:
grass-spread: 4
entities:
spawning:
non-player-arrow-despawn-rate: 20
Grass spread checks run constantly and nobody notices a large dirt patch greening slightly slower. Mob-fired arrows can't be picked up by players anyway, so despawning them after a second instead of a minute removes a category of entity that accumulates in every skeleton-heavy area for no benefit whatsoever.
The Setting Behind the Eight-Second Freeze
If your server occasionally locks up hard — not a TPS dip, a full stop — and it correlates with players trading with cartographer villagers, this is why.
misc:
treasure-maps:
enabled: false
Generating a treasure map means locating a structure. If that structure sits in a chunk that doesn't exist yet, the server generates terrain synchronously, on the main thread, until it finds one. The reference puts it plainly: this is extremely expensive and can hang a server outright.
It is only safe to leave enabled if you have pre-generated your world and set a vanilla world border, so the search can't wander into ungenerated terrain. If you haven't done both, turn it off. Players lose access to a niche item; you stop having unexplained freezes nobody can reproduce.
There's a related pair worth setting either way:
misc:
treasure-maps:
find-already-discovered:
loot-tables: true
villager-trade: true
That lets the server hand out a map to a structure it already knows about instead of hunting for a fresh one.
Four More Settings, Ranked by What They Cost You
Stop mobs re-pathing on every block update.
entities:
behavior:
update-pathfinding-on-block-update: false
By default every block change triggers pathfinding recalculation for nearby mobs. On a redstone-heavy or farm-heavy server that is an enormous amount of repeated work. Disabled, mobs update their path passively every 5 ticks — a quarter second. They may look marginally less sharp; you get a large amount of tick budget back.
Slow down villager sensors. Following on from lobotomisation: if you want villagers alive but cheaper, you can throttle how often their most expensive behaviours fire.
tick-rates:
behavior:
villager:
validatenearbypoi: 60
acquirepoi: 120
sensor:
villager:
secondarypoisensor: 80
nearestbedsensor: 80
playersensor: 40
nearestlivingentitysensor: 40
acquirepoi is the heaviest villager behaviour by a wide margin, which is why it gets pushed out to 120 ticks. If villagers start struggling to find workstations, walk it back down.
Despawn farm debris faster.
entities:
spawning:
alt-item-despawn-rate:
enabled: true
items:
cobblestone: 300
netherrack: 300
sand: 300
gravel: 300
dirt: 300
short_grass: 300
Items default to five minutes on the ground. Cobblestone from a stone generator, or leaves from a tree farm, do not need five minutes — 300 ticks is fifteen seconds. This is targeted at exactly the material that accumulates in thousands and that nobody was going to pick up.
Throttle spawners and grass.
tick-rates:
mob-spawner: 2
grass-spread: 4
Spawners ticked every 2 ticks instead of every tick, grass spread every 4. Both are invisible in play. Push mob-spawner too high relative to your spawners' delay and spawn rates start dropping, so 2 is the safe setting rather than the maximum.
Pre-generate the World
The largest single source of TPS spikes on a young server is terrain generation, and it happens at the worst possible moment — while players are exploring and paying attention.
/chunky radius 5000
/chunky start
That covers most of what a 20–30 player server will ever touch. It takes hours and it is entirely worth it. If you run a world border, generate to the border instead:
/worldborder set 20000
/chunky worldborder
/chunky start
Budget the disk, and you can compute it rather than guess. A region file holds 32×32 chunks, a chunk is 16×16 blocks, and a fully-generated region lands somewhere around 4–8MB depending on terrain complexity:
radius 2,000 -> 62,500 chunks | 61 regions | ~0.2-0.5 GB
radius 5,000 -> 390,625 chunks | 381 regions | ~1.5-3.0 GB
radius 10,000 -> 1,562,500 chunks | 1,526 regions | ~6.0-12 GB
Per dimension. Pre-generating the Nether and End as well roughly doubles it, though the Nether compresses better and the End is mostly void.
Note how badly this scales: doubling the radius quadruples everything, because you're generating area. Radius 10,000 is four times the disk and four times the hours of radius 5,000, for terrain most servers never visit. Pick the smallest radius that covers where players actually go, set a world border there, and let treasure maps stay enabled.
Measure MSPT, Not TPS
Install Spark before you change anything else, because everything above is guesswork without it.
TPS is a poor metric. It's capped at 20, it's averaged, and a server that reports a comfortable 19.8 can be delivering a genuinely bad experience if individual ticks spike to 200ms. MSPT — milliseconds per tick — is the real number. Under 50 means you're keeping up. The distribution matters more than the mean.
/spark health
/spark profiler start
... wait 2-3 minutes while it's actually lagging ...
/spark profiler stop
The profiler gives you a flame graph attributing tick time to actual method calls. What you're looking for:
- Chunk generation high in the graph — you didn't pre-generate, or your view distance is too high
- Entity ticking — activation ranges too generous, or a farm has accumulated more than you think
- A specific plugin — you have your answer
- Hopper ticking — raise the tick intervals above
- Redstone — somebody built something; find it
And/spark gcmonitorafter any flag change, for ten minutes, to confirm the flags did what you think.
Entities Are the Usual Culprit
Entity AI runs on the main thread. Every mob, item, XP orb and armour stand in a loaded chunk gets processed every tick. Three thousand entities means three thousand AI cycles inside a 50ms budget, and past some threshold one thread cannot do it.
Mob caps live in bukkit.yml:
spawn-limits:
monsters: 20
animals: 5
water-animals: 2
water-ambient: 2
water-underground-creature: 3
axolotls: 3
ambient: 1
The maths is playercount x limit, so these scale with how busy you are. Twenty monsters per player sounds brutally low against the vanilla default of 70, and it works — because of the setting most people miss alongside it, in spigot.yml:
mob-spawn-range: 3
That shrinks the radius mobs spawn in around each player. Cut the cap and the radius together and it feels like there are more mobs nearby, not fewer, because the ones you're allowed have nowhere else to go. Keep it at or below your simulation distance.
And the setting that makes the whole thing work, in the Paper world config:
entities:
spawning:
per-player-mob-spawns: true
Without this, the mob cap is global. One player builds a farm, that farm fills the entire server's mob budget, and everyone else stands in an empty world wondering where the monsters went. With it, each player gets their own allowance — spawning behaves the way it does in singleplayer, and that's what lets you drop the caps this low without the world feeling dead. It costs a very small amount of performance and buys back far more in the limits it permits.
Cap, radius, per-player budget. All three or none — cutting the cap alone is what makes a server feel empty.
If you're on Paper you can also set mob limits per world in the Paper world config, which overrides bukkit.yml.
Despawn Ranges Follow From Your Simulation Distance
Mobs outside the hard despawn range vanish instantly; between soft and hard they get a random chance each tick. Set these too generously and you're paying to tick mobs nobody can reach.
The hard range isn't a taste question — it's derived from the simulation distance you already picked:
hard range = (simulation-distance x 16) + 8
Sixteen blocks per chunk, plus a small margin so mobs don't evaporate the instant a player steps one chunk back. Run it against the simulation-distance=4 from earlier:
sim-distance hard range soft
3 56 30
4 72 30
5 88 30
6 104 30
8 136 30
Four chunks gives you 72, which is exactly the recommended default. That's not a coincidence — the published default assumes a simulation distance of 4. If you raise simulation distance and don't raise this, mobs will despawn inside the area you're still paying to tick.
entities:
spawning:
despawn-ranges:
monster:
hard: 72
soft: 30
creature:
hard: 72
soft: 30
Keep soft at 30 and move hard with the formula. The margin works because of delay-chunk-unloads-by, which keeps chunks resident for a few seconds after a player leaves — without that grace period, walking backwards one chunk would delete the mobs behind you.
Two specific offenders worth knowing about.
XP orbs. Each orb is a full entity. A player at a mob farm generates them by the thousand. The merge-radius setting above is what stops this becoming a tick problem — it is not optional on a farm server.
Villagers. Villager AI is among the most expensive computations in the game. They pathfind to beds, workstations and each other continuously. Thirty villagers in a trading hall cost dramatically more than thirty zombies in the same space. If your players build large halls, that's the first place to look when MSPT climbs and nothing else explains it.
Every Plugin Runs on Your Tick Budget
Plugins are code on the main thread. Some are free. Some eat twenty percent of your tick doing something that should be instant.
Reasonable defaults: Spark (profiling, no idle cost), LuckPerms (async where it can be), EssentialsX, CoreProtect (async logging).
Be suspicious of anything advertising itself as an "optimiser" — most of them fight Paper's own scheduling. Also economy plugins doing synchronous SQL on the main thread, anti-cheats that inspect every action, and anything spawning particles at scale.
The discipline that matters: install Spark first, profile a baseline, install the plugin, profile again. If MSPT moved, you know exactly what moved it. Do this before you have twenty people online, not after.
Back It Up Before You Need To
#!/bin/bash
SERVER_DIR="/opt/minecraft/survival"
BACKUP_DIR="/opt/minecraft/backups"
DATE=$(date +%Y%m%d_%H%M)
tar -czf "$BACKUP_DIR/survival_$DATE.tar.gz" \
--exclude="$SERVER_DIR/logs" \
--exclude="$SERVER_DIR/cache" \
"$SERVER_DIR"
ls -t "$BACKUP_DIR"/*.tar.gz | tail -n +15 | xargs -r rm -f
0 4 * * * /opt/minecraft/backup.sh >> /opt/minecraft/backup.log 2>&1
Daily at 4AM, fourteen kept. Note the -r on xargs — without it, an empty backup directory makes rm error every night until somebody reads the log.
Pair it with CoreProtect for block-level rollback. Backups recover from disk failure; CoreProtect recovers from the player who found a hole in your permissions at 2AM.
What to Check Before You Open the Gates
Idle the server with nobody online and run /spark health. MSPT should be under 5ms. If it's over 10 with an empty server, a plugin is doing work it shouldn't.
Join alone and walk into unexplored terrain while watching MSPT. Spikes to 100ms+ mean your pre-generation didn't cover enough ground.
Build a test farm, let it accumulate, and watch MSPT climb. That tells you your entity budget before twenty players find it for you.
Then read the GC section of /spark health. Frequent pauses over 100ms mean the flags or the heap are wrong. And if the GC numbers look fine while players still complain — go back and read the safepoint log, because the pause you're looking for isn't in the GC log at all.