Spark is the standard profiling tool for Minecraft servers. Created by lucko as a fork of sk89q’s WarmRoast, it’s licensed under GPL-3.0 and now ships bundled with Paper 1.21+.
Unlike instrumentation-based tools (which wrap every method call with timers), Spark uses statistical sampling: it takes stack snapshots at configurable intervals, defaulting to every 4 milliseconds. This means Spark adds almost zero overhead to your server. The tradeoff is that sampling shows proportional time rather than exact call counts, but for diagnosing lag, proportional time is exactly what you want.
Spark supports two sampling engines. On Linux and macOS, it uses async-profiler, a native agent that captures stack traces without safepoint bias, giving more accurate results. On Windows (or when forced with --force-java-sampler), it falls back to a Java-based sampler using ThreadMXBean. Both produce the same viewer output, but async-profiler captures frames that the Java sampler can miss.
Installation
Paper 1.21+: Spark is bundled. No plugin jar needed. Just run /spark in the console or in-game. If you need to override the bundled version with a newer plugin release, add -Dpaper.preferSparkPlugin=true to your JVM flags.
Spigot / older Paper: Download the Bukkit plugin from spark.lucko.me and drop it in your plugins/ folder.
Fabric / Forge / NeoForge: Install spark as a mod. Same download page, different artifact.
Proxies: Spark supports BungeeCord and Velocity as well.
Commands vary by platform:
| Platform | Command prefix |
|---|---|
| Server (Bukkit/Paper/Spigot) | /spark |
| Client (Fabric/Forge mod) | /sparkc |
| BungeeCord | /sparkb |
| Velocity | /sparkv |
Running a profile
The basic command starts a CPU profile for 10 minutes:
/spark profiler start --timeout 600
After the timeout (or when you run /spark profiler stop), Spark uploads the report to spark.lucko.me and gives you a shareable link.
Targeting lag spikes
A standard profile averages all ticks together. If your server runs fine 95% of the time but stutters occasionally, the spike data gets drowned out by normal ticks. To isolate spikes, use:
/spark profiler start --only-ticks-over 100
This records samples only from ticks that exceed 100ms (double the 50ms budget). The resulting profile shows only what happens during the bad ticks, making the cause immediately visible. The Spark docs recommend setting this value lower than the actual spike duration, somewhere between 50 and 100ms for most situations.
Before profiling, use /spark tickmonitor to watch individual tick durations in real time. This tells you whether spikes are actually happening and how severe they are.
Other useful flags
--thread *profiles all threads, not just the server thread. Useful for diagnosing async plugin work or chunk loading threads.--allocswitches from CPU profiling to memory allocation profiling. Instead of measuring where time is spent, it measures where objects are allocated. Essential for diagnosing GC pressure.
Monitoring commands
These don’t produce profiles but give quick health snapshots:
| Command | What it shows |
|---|---|
/spark tps | Current TPS with 5s, 10s, 1m, 5m, 15m averages |
/spark health | CPU, memory, disk, and network summary |
/spark gc | Garbage collector type and recent pause statistics |
/spark gcmonitor | Live GC event feed (toggle on/off) |
/spark tickmonitor | Live per-tick duration reporting |
Understanding TPS and MSPT
Every Minecraft server runs a tick loop. One tick is budgeted at 50 milliseconds, giving a target rate of 20 ticks per second (TPS). When a tick takes longer than 50ms, the server cannot maintain 20 TPS, and players experience lag: block break delays, rubber-banding, slow mob AI. For a deeper look at what runs inside that single-threaded loop versus what Mojang has already moved off it, see Is a Minecraft Server Single-Threaded? The Real Architecture, Explained.
TPS thresholds
| TPS | Severity |
|---|---|
| 20.0 | Healthy |
| 18.0 - 19.9 | Minor lag, most players won’t notice |
| 15.0 - 17.9 | Moderate lag, noticeable delays |
| Below 15.0 | Severe, the server is struggling |
MSPT is more diagnostic than TPS
TPS tells you whether the server is keeping up. MSPT (milliseconds per tick) tells you how much work each tick is doing. Spark reports four MSPT values: min, median, 95th percentile (p95), and max.
The p95 is the single most useful metric. Here’s why averages lie:
Imagine a server where 95% of ticks take 20ms and 5% take 200ms. The average MSPT is roughly 29ms, which looks comfortable. TPS might even read 19.5 because the server catches up during the fast ticks. But the p95 is 200ms, revealing that players experience a 200ms freeze every second. That’s the lag they’re complaining about, and the average hid it completely.
waitForNextTick() in profiles
When you open a Spark profile, you’ll often see a method like waitForNextTick() or equivalent sleep/park calls taking a significant percentage of the server thread. This is healthy. It means the server finished its tick work early and is waiting for the next 50ms window. The Spark docs describe these thresholds:
- Above 20% idle: healthy, plenty of spare capacity
- 5% to 20% idle: working hard, occasional lag possible under load spikes
- Below 5% idle: the server is at capacity with no buffer
If you see zero idle time, every tick is at or over budget. That server is lagging constantly.
The Spark Viewer
When you open a Spark report link, the viewer loads with several panels.
Overview panel
The top section shows server metadata: TPS history, MSPT statistics, CPU usage, memory allocation, platform and version info, JVM flags, and the plugin/mod list. Check this first. Many performance problems are visible here before you even look at the profile: wrong Java version, missing Aikar flags, heap too small, GC pauses too frequent.
All View (call tree)
This is the default view. It shows the full call stack as an expandable tree, with each node showing what percentage of total samples it accounts for.
Navigation strategy: start at the root and expand the highest-percentage child at each level. For the server thread, the typical path is:
Thread: Server thread
> Thread.run
> MinecraftServer.run (or tickServer)
> MinecraftServer.tickChildren
> ServerLevel.tick (per-world ticking)
> [subsystem methods]
At some point the tree branches into multiple children. This is where diagnosis begins. If EntityTickList.forEach takes 45% and ChunkMap.tick takes 15%, entities are your primary problem.
Flat View
The flat view extracts the top 250 individual methods, sorted by time. Two sort modes matter:
- Total Time: time spent in this method and everything it calls. Use this to find expensive branches high in the call stack.
- Self Time: time spent in this method’s own code only, excluding calls to other methods. Use this to find the actual bottleneck: the leaf node doing the real work.
If PathFinder.findPath shows 30% Total Time but only 2% Self Time, the pathfinding algorithm itself is fast, but it’s calling something expensive underneath it. Switch to Self Time to find that something.
Sources View
This view groups profile data by source: which plugin, mod, or vanilla Minecraft code owns each method. It answers “how much server time does each plugin consume?”
One important caveat: attribution follows the call stack, not causation. If Plugin A fires a custom event that triggers a chunk load, the chunk loading cost appears under Plugin A’s tree, even though the chunk system is vanilla code. The Sources view shows who initiated the work, not necessarily who wrote the slow code.
Flame Graph
The flame graph is an alternative visualization of the same data. Each bar represents a method. Width is proportional to time spent: wider bars consumed more of the profile. Height represents call depth: the bottom is the entry point, and each layer up is a deeper method call.
Click any bar to zoom into that subtree. This is the fastest way to visually identify which branches dominate the profile.
Note: Spark’s flame graph colors are aesthetic, used to visually distinguish adjacent bars. They do not encode semantic categories (the colors are not mapped to “vanilla vs. plugin vs. Java standard library”). Use the method names and package paths to identify code origin, not bar color.
Common lag patterns
Entity ticking
Signature: EntityTickList.forEach or Level.tickNonPassenger consuming more than 30% of the server thread. Expanding further reveals Mob.tickAi, PathFinder.findPath, or GoalSelector.tick.
Cause: too many loaded entities, usually mobs. Mob AI pathfinding is the most expensive per-entity operation.
Fix: reduce mob caps (bukkit.yml spawn limits), use entity-activation-range in paper-world.yml (Paper) to skip AI for distant mobs, or investigate mob farms that prevent natural despawning. For a real profile showing this pattern at two different severities on the same server, see our Cobblemon/AllTheMons entity overload case study.
Chunk loading and generation
Signature: ChunkMap.tick or ChunkGenerator methods taking more than 20% of the profile, or significant time in ThreadedAnvilChunkStorage.
Cause: players exploring new terrain, or plugins force-loading chunks on the main thread.
Fix: pre-generate your world with Chunky, reduce view distance, or check if a plugin is calling getChunkAt() synchronously.
Plugin lag (synchronous I/O)
Signature: a plugin’s package name appears in the call tree with methods like java.sql.DriverManager.getConnection, java.net.Socket.connect, java.io.InputStream.read, or JDBC calls (executeQuery, executeUpdate) on the server thread.
Cause: the plugin is making database queries or HTTP requests on the main thread. Every millisecond spent waiting for a network response is a millisecond the tick cannot finish.
Fix: the plugin needs to move I/O to async threads. If it’s a plugin you control, use Bukkit.getScheduler().runTaskAsynchronously(). If it’s a third-party plugin, file a bug report or find an alternative.
A related pattern worth knowing: Commands.performCommand or ExecuteCommand methods dominating the tree usually means a datapack or plugin is running /execute logic every tick. See our case study on a datapack silently dropping TPS on a 5-player server for a worked example, including how call-tree analysis catches this when config checkers can’t.
GC pressure
Signature: not visible in a standard CPU profile. Use /spark gc to check GC pause frequency and duration. Use /spark gcmonitor to watch GC events in real time. If GC accounts for more than 5% of wall time, it’s a concern. Above 15% is critical.
To find what’s generating garbage, run an allocation profile:
/spark profiler start --alloc --timeout 300
The resulting report shows which methods allocate the most objects. Common offenders: plugins creating excessive Location or ItemStack objects per tick, poorly written particle systems, or world serialization code.
If GC pauses are the actual problem rather than allocation rate, the fix is usually JVM flags rather than code changes. See Aikar’s JVM Flags Explained for what each G1GC tuning flag does, or Best JVM Flags for Minecraft Servers in 2026 for a comparison of G1GC, ZGC, and Shenandoah by heap size.
Redstone and hoppers
Signature: LevelTicks.tick with time in RedstoneWireBlock.updatePowerStrengthForNode, or HopperBlockEntity.tryMoveItems / HopperBlockEntity.serverTick.
Cause: large redstone contraptions or hopper chains. Each hopper checks for items every tick. A chain of 100 hoppers is 100 inventory scans per tick.
Fix: for hoppers, Paper’s hopper.disable-move-event configuration helps significantly. For redstone, consider alternate redstone implementations (Paper has alternate-current support that replaces vanilla’s O(n^2) wire updates with a more efficient algorithm).
Network / packet handling
Signature: ServerGamePacketListenerImpl or Connection.tick consuming unusual time.
Cause: rarely a problem unless you have very high player counts (100+) or plugins sending excessive packets (particle effects, scoreboard updates every tick). Usually this is a symptom rather than a root cause.
Spark vs. other tools
vs. Timings
Timings was Paper’s built-in instrumentation profiler. It wrapped event handlers and tick phases with timers, giving counts and total time per event type. Spark uses sampling instead, which captures the full call stack down to individual Java methods.
Timings told you “EntityMoveEvent took 12ms across 400 calls.” Spark tells you which specific code path inside that event handler is slow and exactly where in the call stack it sits.
Paper began removing Timings in favor of bundled Spark, tracked in PaperMC/Paper discussion #10565. In Paper 1.21+, Spark is the built-in profiler and Timings is no longer available.
vs. rule-based config checkers
Some free tools take a rule-based approach (not AI or LLM): they read Spark report metadata like server version, JVM flags, plugin list, and config files. They check whether your paper-world.yml has good values, whether you’re using Aikar’s flags, and similar configuration checks.
What this approach does not do: it never reads the call tree, the flame graph, or the profiling data itself. It cannot tell you which entity type is lagging your server or which plugin method is blocking the main thread. Some also gate their output behind a TPS check, suppressing recommendations once your average TPS crosses a threshold.
Config checkers are useful for catching configuration mistakes. For actual performance diagnosis, you need something that reads the profiling data.
Zerve Spark Analyzer
Zerve’s free analyzer goes further: it parses the call tree and timing data from your Spark report to identify lag patterns, hotpaths, and bottlenecks. No login required, no token limit, shareable permanent links.
Conclusion
Reading a Spark report is a learnable skill, not a dark art. Start with the overview (TPS, MSPT p95, GC stats). Open the call tree and follow the biggest percentages. Use the flat view to find where Self Time concentrates. Check the Sources view to see which plugins consume the most time. Profile spikes separately with --only-ticks-over.
The profiler gives you data. The skill is knowing what questions to ask: Is idle time near zero? Is one subsystem dominating? Is there synchronous I/O on the main thread? Is GC eating into tick time? Once you know the question, the Spark viewer will show you the answer.
Sources
- spark documentation: Command Usage
- spark documentation: Installation
- spark documentation: Using the Viewer
- spark documentation: TPS and MSPT
- spark documentation: The Tick Loop
- spark documentation: Finding Lag Spikes
- spark documentation: spark vs. Others
- spark GitHub repository (GPL-3.0)
- PaperMC profiling documentation
- PaperMC/Paper discussion #10565: Timings removal