zerve.gg
Back to blog

How to Read a Spark Profiler Report: A Complete Guide

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:

PlatformCommand 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

Monitoring commands

These don’t produce profiles but give quick health snapshots:

CommandWhat it shows
/spark tpsCurrent TPS with 5s, 10s, 1m, 5m, 15m averages
/spark healthCPU, memory, disk, and network summary
/spark gcGarbage collector type and recent pause statistics
/spark gcmonitorLive GC event feed (toggle on/off)
/spark tickmonitorLive 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

TPSSeverity
20.0Healthy
18.0 - 19.9Minor lag, most players won’t notice
15.0 - 17.9Moderate lag, noticeable delays
Below 15.0Severe, 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:

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:

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