Bukkit, Spigot, Paper, Pufferfish, Purpur, Folia, and Leaf: The Complete Minecraft Server Software Guide (2026)
The full history and technical breakdown of every major Minecraft server software — from Bukkit's 2010 plugin API design through Paper's 2024 hard fork from Spigot, Pufferfish's DAB algorithm, Folia's ThreadedRegionizer, and Leaf's collection optimizations. With code.
There is a fork tree spanning fifteen years underneath every Minecraft server running today. Most admins copy a jar file and start the server without knowing what they're actually running, how it got there, or what the code is doing differently from everything upstream of it.
This article covers the full chain. Bukkit's API design. CraftBukkit's implementation. Spigot's patches and why they're losing relevance. Paper's December 2024 hard fork and what switching to Mojang mappings means in practice. Pufferfish's DAB algorithm explained at the math level. Purpur's configurability system. Folia's threading architecture. Leaf as the current example of a community-maintained kitchen-sink fork. And the notable discontinued projects whose code still runs inside everything you're using today.
All of it with actual code.
2010: Where the Whole Thing Started
Back in 2010, running a Minecraft server meant patching the vanilla server jar directly. If you wanted custom behavior — chat commands, permissions, teleportation — you modded bytecode. There was no plugin system. Every server fork was incompatible with every other fork. Keeping up with Mojang's updates meant redoing all your patches from scratch.
The Bukkit team solved this by separating two things that had been tangled together: the plugin API and the server implementation. Bukkit was the API. CraftBukkit was the implementation that ran the server and called into that API. Plugins wrote against Bukkit. They ran on CraftBukkit. When Mojang updated the game, only CraftBukkit needed to change. Plugins stayed stable.
That separation is the foundation everything else is built on, fifteen years later.
The Bukkit API: What It Actually Is
Bukkit is a Java interface specification. A plugin interacts with the game through Bukkit's types and methods. The implementation behind those types can be anything as long as it satisfies the contract.
The core entry point is JavaPlugin, which every plugin extends:
// Source: org.bukkit.plugin.java.JavaPlugin
// https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/plugin/java/JavaPlugin.html
//
// Every Bukkit plugin starts here. onEnable() fires when the server loads
// the plugin. All your initialization — registering event listeners,
// commands, scheduled tasks — goes in this method.
public class MyPlugin extends JavaPlugin {
@Override
public void onEnable() {
// Register this class as a listener for game events
getServer().getPluginManager().registerEvents(this, this);
// Register a command defined in plugin.yml
getCommand("hello").setExecutor(this);
// Schedule a repeating task — fires every 100 ticks (5 seconds)
getServer().getScheduler().scheduleSyncRepeatingTask(this, () -> {
getServer().broadcastMessage("Server is alive.");
}, 0L, 100L);
}
@Override
public void onDisable() {
// Cleanup on shutdown
}
}
The getServer() call returns a Server interface. That interface has 200+ methods covering everything from world manipulation to player lookup to scheduler access. CraftBukkit implements Server with CraftServer. Paper implements it with modifications. Every fork in the entire ecosystem provides its own implementation of the same interface.
Event handling works through a registration and dispatch system. Plugins register listeners with PluginManager. The server calls PluginManager.callEvent() on every game action. Registered listeners fire in priority order:
// Source: org.bukkit.event.EventHandler / org.bukkit.event.Listener
// https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/event/EventHandler.html
//
// The @EventHandler annotation marks methods the event system will call.
// priority controls order when multiple listeners handle the same event.
// ignoreCancelled skips this handler if a higher-priority listener cancelled.
public class MyListener implements Listener {
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onPlayerJoin(PlayerJoinEvent event) {
event.getPlayer().sendMessage("Welcome, " + event.getPlayer().getName());
}
@EventHandler(priority = EventPriority.HIGHEST)
public void onBlockBreak(BlockBreakEvent event) {
// HIGH and HIGHEST run after NORMAL — can override earlier decisions
if (event.getBlock().getType() == Material.BEDROCK) {
event.setCancelled(true);
}
}
}
This design, from 2010, is what every fork from Spigot to Folia builds on or diverges from. Understanding it tells you why the API breaks when Folia eliminates the main thread — scheduleSyncRepeatingTask in JavaPlugin schedules to the main thread by definition. There is no such thing in Folia.
CraftBukkit: The Bridge Layer
CraftBukkit wraps Minecraft's internal server classes with Bukkit API types. Every Player in your plugin is actually a CraftPlayer wrapping a ServerPlayer. Every Block wraps a BlockPos and a LevelChunk. Every world operation goes through translation layers that convert between Bukkit's stable API types and Minecraft's internal NMS (Net Minecraft Server) types.
The NMS type names and packages change with every Minecraft version. CraftBukkit's job is absorbing those changes so plugin code doesn't have to.
Spigot maintains CraftBukkit today as the base layer of their distribution. Paper absorbed CraftBukkit into their project during the hard fork, and now maintains that translation layer independently.
Spigot: The First Major Performance Fork
Spigot forked CraftBukkit in 2012 to fix performance problems that the original team wasn't prioritizing. The key improvements it shipped:
Entity and tile entity activation ranges. Entities far from players tick less frequently. This is the conceptual predecessor to Paper's more sophisticated Entity Activation Range 2.0.
Chunk loading optimizations. Reduced the work done when loading chunks from disk, batched chunk sends to clients, and introduced configurable view distance separate from simulation distance.
spigot.yml. A configuration file exposing knobs that vanilla's server.properties didn't offer: mob spawn rates, entity activation ranges, bundle packet sizes, Netty thread counts.
Spigot also introduced the BuildTools compilation requirement as a legal workaround. After Bukkit's 2014 DMCA dispute, Spigot could not distribute pre-built jars containing Mojang's code. BuildTools downloads the vanilla jar, decompiles it, applies Spigot's patches locally on your machine, and produces the final Spigot jar. You build it yourself. Technically, Spigot never distributes Mojang's code directly.
Why Spigot Is Losing Relevance
On December 13, 2024, Paper officially hard-forked from Spigot. Before that date, Paper pulled all Spigot updates into its own codebase before applying its own patches on top — Paper was always Spigot plus more. That dependency meant Paper's release cycle was gated on Spigot releasing first, since Spigot's development process is closed-source.
Paper's hard fork announcement stated it plainly:
Since the project's inception, Paper has been built on top of Spigot... As a result of such divergence, our strict policy to stay up-to-date with Spigot has been limiting the project, most noticeably with slower version updates since its updates to snapshots, pre-releases and release candidates are worked on behind closed doors.
— PaperMC Hard Fork Announcement (https://papermc.io/news/the-future-of-paper-hard-fork/?ref=coderoasis.com)
Starting with Minecraft 1.21.4, Paper applies Mojang's changes independently. It no longer waits for Spigot. It switched to official Mojang mappings, which means NMS class names are now stable and human-readable instead of obfuscated. Plugins using Paper-API won't run on Spigot if they use Paper-specific API — and Paper holds an 85-90% market share across modern Minecraft versions according to bStats data from major plugins.
Spigot's remaining case is backwards compatibility. It will continue receiving updates, it works fine, and very old plugins may run on it that don't run elsewhere. For any new server, there is no reason to start on Spigot.
Paper: The Current Standard, Now With Mojang Mappings
Paper today is its own independent project applying ~1,600 patches and over 130,000 lines of additional code on top of Mojang's vanilla server. Beyond what we covered in the previous Paper article, the Mojang mappings switch is worth understanding for anyone writing plugins or reading Paper's source.
Before the hard fork, NMS class names were obfuscated. A class that in Mojang's source was called ServerPlayer might appear as EntityPlayer in CraftBukkit's mappings, net.minecraft.server.v1_21_R1.EntityPlayer in the package-relocated version Paper shipped with, and something else entirely in the raw decompiled output. Every Minecraft update could rename classes arbitrarily. Plugin developers who accessed NMS directly to do things the Bukkit API didn't expose had to rewrite reflection calls on every update.
With Mojang mappings, class names match what Mojang actually calls them in their source. ServerPlayer, Level, BlockPos, ChunkMap. These names are stable across versions if Mojang doesn't rename them. Reflection-based NMS access now works across minor updates without changes. The Java 26 article covers why this also synergizes with Java's improvements to the JVM internal lookup mechanisms.
Paper's event API also extends Bukkit's with async variants for read-only operations:
// Source: io.papermc.paper.event.player.AsyncChatEvent
// https://docs.papermc.io/paper/dev/api/event-api/
//
// Paper's async chat event fires off the main thread, reducing
// the cost of chat processing under heavy player load.
// Read-only access to player and message is safe here.
// Any world-state modification must be scheduled back to the main thread.
public class ChatListener implements Listener {
@EventHandler
public void onAsyncChat(AsyncChatEvent event) {
// This handler runs off the main thread.
// Safe: reading player name, filtering message content.
// Unsafe: calling world.setBlock(), player.teleport(), etc.
Player player = event.getPlayer();
Component message = event.message();
// Run a profanity filter off the main thread
if (containsProfanity(message)) {
event.setCancelled(true);
// Schedule feedback back to main thread for world interaction
Bukkit.getScheduler().runTask(plugin, () -> {
player.sendMessage(Component.text("Message blocked."));
});
}
}
}
The distinction matters for JVM garbage collection pressure. Chat events that fire synchronously on the main thread block the tick. On a 100-player server with active chat, the cumulative cost is measurable. Paper's async chat event moves that cost off the critical path.
Pufferfish: Performance Without Vanilla Compromise
Pufferfish is a Paper fork built by the team behind Pufferfish Host, targeting large servers where performance headroom is more critical than strict vanilla parity. It adds three categories of patches that Paper doesn't include: the DAB system, SIMD map rendering, and async pathfinding (in the paid Pufferfish+ variant).
DAB: Distance-Adjusted Brain Ticking
DAB stands for Distance-Adjusted Brain ticking. It's one of the most impactful optimizations available for servers with many entities. The problem it solves: entity "brains" — the AI goal system that governs villager schedules, mob behavior, and pathfinding decisions — are expensive. The vanilla server runs a full brain tick for every entity every tick, regardless of how far they are from any player.
A villager restocking trades at 300 blocks from the nearest player runs the same brain processing as a villager the player is actively trading with. That's waste that compounds with entity count.
DAB's formula is published in their configuration docs. The tick frequency for an entity is:
freq = distanceToPlayer² / 2^activation_dist_mod
With the default activation_dist_mod of 8:
At 10 blocks: freq = 100 / 256 = 0.39 → ticks every ~2.6 server ticks
At 32 blocks: freq = 1024 / 256 = 4.0 → ticks every 4 server ticks
At 64 blocks: freq = 4096 / 256 = 16.0 → ticks every 16 server ticks
At 128 blocks: freq = 16384 / 256 = 64.0 → ticks every 64 server ticks (once every ~3 seconds)
The max-tick-freq setting caps how infrequently entities tick regardless of distance, preventing distant entities from becoming completely inert. The start-distance setting creates an inner radius within which no DAB throttling applies — entities within that radius always tick fully.
In pufferfish.yml:
# Source: Pufferfish configuration
# https://docs.pufferfish.host/setup/pufferfish-fork-configuration/
dab:
enabled: true
start-distance: 12 # Within 12 blocks of a player: no throttling
activation-dist-mod: 8 # The exponent in the freq formula (default 8)
max-tick-freq: 20 # Never tick less often than every 20 ticks (1 second)
blacklisted-entities: # These entities always tick at full rate
- armor_stand
- item_frame
- villager # Some servers exclude villagers for trade accuracy
# Disable goal selection for inactive entities entirely
inactive-goal-selector-disable: true
# Strip out the built-in method profiler overhead in production
disable-method-profiler: true
The blacklist deserves attention. Villager restocking, breeding cooldowns, and profession changes all depend on brain ticks. If you run a server with player-managed villager trading halls, you may want to blacklist villager to prevent trade desync. The tradeoff: all your villagers tick at full rate regardless of distance, eliminating the CPU savings for them specifically. Profile with Spark first to see if villager brain ticks are actually in your flame graph before deciding.
SIMD Map Rendering
Pufferfish accelerates MapCanvas rendering using SIMD (Single Instruction, Multiple Data) CPU instructions through Java's Vector API. This matters for servers running image map plugins like ImageOnMap or similar tools that display custom images on in-game maps.
The vanilla map rendering pipeline processes each pixel individually — one byte operation per pixel, per frame, per player looking at the map. On a server with dozens of active map displays, this adds up to measurable main-thread time.
Java's Vector API (which has been in preview since Java 16 and received significant stability improvements in Java 25) allows processing 16 or 32 bytes in a single CPU instruction. Pufferfish's patch rewrites the inner rendering loop to use ByteVector operations:
// Conceptual representation of Pufferfish's SIMD map rendering approach
// Source: pufferfish-gg/Pufferfish patches
// https://github.com/pufferfish-gg/Pufferfish
// Vanilla: processes one pixel at a time
for (int i = 0; i < pixels.length; i++) {
output[i] = colorMap[pixels[i] & 0xFF]; // Single byte per iteration
}
// Pufferfish SIMD: processes 16+ bytes per CPU instruction
// using Java's jdk.incubator.vector.ByteVector
VectorSpecies<Byte> SPECIES = ByteVector.SPECIES_256;
int loopBound = SPECIES.loopBound(pixels.length);
for (int i = 0; i < loopBound; i += SPECIES.length()) {
ByteVector chunk = ByteVector.fromArray(SPECIES, pixels, i);
// Apply color mapping to entire vector simultaneously
chunk.intoArray(output, i);
}
// Handle remaining pixels outside the vector boundary
for (int i = loopBound; i < pixels.length; i++) {
output[i] = colorMap[pixels[i] & 0xFF];
}
The reported improvement is up to 8x faster map rendering for canvas operations. If your server doesn't use image map plugins, this patch costs nothing and gains nothing. If it does, the difference is noticeable in Spark profiles.
Pufferfish+ and Async Pathfinding
Pufferfish+ is the paid tier that adds full async pathfinding and async entity tracking on top of the free Pufferfish optimizations. These move the A* pathfinding computation and entity position tracking off the main thread entirely.
The reported improvement from Pufferfish+ over standard Pufferfish is 30-35% on most server configurations. That is a significant number. The tradeoff is a $50/month license fee for standalone use, or included in Pufferfish Host plans.
The free version of Pufferfish is still a meaningful improvement over Paper for entity-heavy servers. It's Paper-API compatible — every Paper plugin runs unchanged.
Purpur: The Configuration Layer
Purpur sits above Pufferfish in the inheritance chain. It includes all of Pufferfish's performance patches and adds an extensive purpur.yml configuration system. The goal isn't more performance — Purpur ships with every extra option disabled by default, and the server behavior with everything disabled is identical to Pufferfish.
The purpur.yml options worth knowing about fall into three categories that matter for real server operations:
Entity behavior knobs you can't get from Paper:
# Source: PurpurMC/Purpur - purpur.yml configuration
# https://purpurmc.org/docs/purpur/configuration/
mobs:
villager:
# Lobotomize villagers stuck in 1x1 spaces.
# They waste CPU running pathfinding that always fails.
# Check interval: how often to check if they're stuck (in ticks).
lobotomize:
enabled: true
check-interval: 100
# How often villager brains tick.
# Default Minecraft: every tick. Purpur default: every 8 ticks.
# Raise this carefully — it affects trade restocking, schedule adherence.
brain-ticks: 4
use-brain-ticks-only-when-lagging: true
zombie:
# Zombies won't chase villagers when TPS is below lagging-threshold.
# Reduces cross-entity AI interactions during load spikes.
aggressive-towards-villager-when-lagging: false
creeper:
# Probability a naturally-spawned creeper is charged (0.0 to 100.0)
naturally-charged-chance: 0.0
The lag threshold system:
settings:
# Server is considered lagging below this TPS.
# Enables all the *-when-lagging behavior options.
lagging-threshold: 19.0
This is Purpur's most useful structural addition. When TPS drops below lagging-threshold, behaviors tagged as -when-lagging engage automatically. You configure which gameplay fidelity you sacrifice first under load. Zombie-villager aggression costs CPU. Villager brain ticks cost CPU. Defining a clear degradation hierarchy means the most critical gameplay systems stay functional when the server is under stress, and secondary AI interactions drop off gracefully.
The rideable mobs and cosmetic options:
mobs:
chicken:
ridable: false
ridable-in-water: false
cow:
ridable: false
pig:
ridable: true # Vanilla behavior — pigs are rideable with saddles
give-saddle-back: false
These are the options that give Purpur its reputation for being "quirky." The actual useful production options are the brain tick throttling, lobotomize, and lag threshold. The rideable cows and flying squids are there if you want them — none of it is on by default.
Purpur's inheritance chain matters for fork choice. Because Purpur includes Pufferfish, running Purpur gets you DAB, SIMD map rendering, and all Pufferfish optimizations without installing Pufferfish separately. If you want Pufferfish-level performance and Purpur's configuration options, Purpur is one jar.
Folia: Eliminating the Main Thread
We covered Folia's ThreadedRegionizer architecture in the previous article. The code-level detail worth going deeper on is what the API breakage looks like in practice and why it's total.
In any pre-Folia server, thread safety for world access is simple: if you're on the main thread, you can do anything. The entire game state is owned by that one thread. Plugins enforce this implicitly by always scheduling world-modifying code with Bukkit.getScheduler().runTask().
Folia's plugin requirements break down to one rule: you must obtain ownership of the region that owns what you're touching before you touch it. The TickThread.isTickThreadFor(entity) check in Folia's source will throw if you violate this:
// Source: PaperMC/Folia - patches/server/0003-Threaded-Regions.patch
// https://github.com/PaperMC/Folia
//
// Folia's thread ownership check. This fires on entity data access
// from the wrong region thread, failing loudly before corruption happens.
public static void ensureTickThread(final Entity entity, final String reason) {
if (!isTickThreadFor(entity)) {
throw new IllegalStateException(
"Thread " + Thread.currentThread().getName() +
" is not the owner of entity " + entity +
": " + reason
);
}
}
// Folia's RegionScheduler — replacement for BukkitScheduler.runTask()
// schedules a task to run on the tick thread that owns a given Location.
// The server parameter is the Server instance, plugin is your JavaPlugin.
Server server = Bukkit.getServer();
// Schedule to run on the tick thread owning (world, 100, 64, 200)
server.getRegionScheduler().run(plugin, world, 100, 200, scheduledTask -> {
// Now on the correct region thread — safe to modify blocks here
world.getBlockAt(100, 64, 200).setType(Material.GOLD_BLOCK);
});
// Schedule to run on the tick thread owning a specific entity
Entity entity = ...;
entity.getScheduler().run(plugin, scheduledTask -> {
// Now on the thread that owns this entity's region
entity.setVelocity(new Vector(0, 1, 0));
}, /* fallback if entity removed before task runs */ () -> {});
The fallback lambda in entity.getScheduler().run() is unique to Folia and represents a real concurrency concern that Paper's scheduler never had to handle. In single-threaded Paper, if you schedule a task for an entity that gets removed before the task runs, the task still fires and your code finds entity.isValid() == false. In Folia, the entity might be removed by a different region thread between when you scheduled the task and when the task would run. The fallback lambda fires instead, giving you a clean place to handle that case.
This is why Folia breaks every plugin: the concurrency model is categorically different. It's not a matter of updating a method call. Plugin authors need to reason about region ownership for every world-state access, which requires understanding Folia's threading model at this level.
Leaf: The Community Kitchen-Sink Fork
Leaf is a Paper fork maintained by the Winds-Studio team that functions as an aggregator of patches from across the ecosystem. Its README credits patches from Pufferfish, Purpur, Luminol, Nitori, Moonrise, Plazma, SparklyPaper, and several discontinued projects. The practical effect: you get a curated selection of community-vetted optimizations from across the ecosystem in one distribution.
Leaf's specific technical contribution beyond aggregation is data structure optimization. It replaces standard Java collection types in performance-critical locations with faster alternatives from the Eclipse Collections and FastUtil libraries:
// Source: Winds-Studio/Leaf
// https://github.com/Winds-Studio/Leaf - patches/features/0050-Replace-AI-attributes-with-optimized-collections.patch
//
// AttributeMap in vanilla uses Object2ObjectOpenHashMap for attribute storage.
// Leaf replaces it with Reference2ReferenceOpenHashMap.
//
// The difference: Object2ObjectOpenHashMap calls .equals() for key comparison.
// Reference2ReferenceOpenHashMap uses reference equality (==), which is
// faster when keys are enum-like singletons that are always the same instance.
// Attribute keys in Minecraft are singleton registry objects — reference
// equality is correct and faster.
// Before (vanilla / Paper):
private final Map<Attribute, AttributeInstance> attributes =
new Object2ObjectOpenHashMap<>();
// After (Leaf):
private final Map<Attribute, AttributeInstance> attributes =
new Reference2ReferenceOpenHashMap<>();
This class of optimization is low-level, transparent, and measurable under profiling without being visible to players. Every entity has an AttributeMap. Every entity AI tick touches attributes. Swapping to reference equality for singleton keys eliminates hash code computation and object .equals() calls across millions of lookups per second.
Leaf also incorporates Pufferfish's DAB and async systems, Purpur's configuration options, and adds its own configuration via leaf-global.yml:
# Source: Winds-Studio/Leaf - leaf configuration
# https://www.leafmc.one/docs/
async:
# Async mob spawning — compute spawn decisions off the main thread
async-mob-spawning:
enabled: true
# Async pathfinding — A* computation on worker threads
async-pathfinding:
enabled: true
max-threads: 0 # 0 = auto-detect based on available cores
# Async entity tracker — entity visibility updates off main thread
async-entity-tracker:
enabled: true
compat-mode: false # Set true if plugins have tracker-related issues
performance:
# Replace Java standard collections with optimized variants
# where Leaf has profiled improvements
use-optimized-collections: true
# Inactive goal selector disable (from Pufferfish)
inactive-goal-selector-disable: true
Leaf is fully compatible with Paper and Spigot plugins — the API surface inherits from Paper unchanged. For servers that want a single jar with a broad set of performance patches without the plugin compatibility cost of Folia, Leaf is the current best example of that approach.
The honest caveat about kitchen-sink forks: Leaf is maintained by a small team and targets newer Minecraft versions. The breadth of patches from many upstream sources increases the surface area for bugs introduced by patch interactions. Running Leaf means relying on the Winds-Studio team to vet those patches and keep them compatible with each other and with each new Paper update. For stable production servers, that's a risk assessment you need to make. For servers where maximizing performance is the priority and you have the operational capacity to test and manage updates, Leaf is worth evaluating.
The Discontinued Forks That Live Inside Everything
Several important projects are dead as standalone distributions but their code runs inside every major fork today.
Tuinity shipped the Starlight chunk lighting engine — a complete rewrite of Minecraft's light propagation algorithm that eliminated most of the lighting-related lag spikes that plagued vanilla and early Spigot servers. Tuinity merged into Paper. Starlight is now part of Paper's core. If you've ever wondered why modern Paper servers handle chunk lighting so much better than older servers did, this is why.
Airplane pioneered the DAB concept under a different name before Pufferfish absorbed and extended it. Airplane's developer discontinued the project and the patents-on-concepts transferred to the community. If you see "DAB" in Pufferfish's docs, Airplane is where it originated.
Mohist and Magma are hybrid forks that try to run both Forge mods and Bukkit plugins simultaneously on the same server. They exist, they work for some combinations, and they're notorious for causing bizarre plugin/mod interactions that neither Paper nor Forge support. Both projects acknowledge this openly. If you need mods and plugins together, test extensively on a staging server before running it in production. These forks receive security updates slowly compared to Paper.
CatServer and Arclight serve the same mod-plus-plugin use case targeting Fabric and Forge respectively. The plugin compatibility story is better for some combinations than Mohist. The same caveat applies: test everything.
The Full Inheritance Chain as a Plugin Developer
If you're writing a plugin, the API you target determines where your plugin runs. Here's the full picture:
Bukkit API (interface specification — Spigot still publishes this)
└── Spigot API (extends Bukkit — adds Spigot-specific types)
└── Paper API (extends Spigot API for now, diverging post-1.21.4)
└── Pufferfish API (extends Paper API — adds pufferfish.yml access)
└── Purpur API (extends Pufferfish API — adds purpur.yml access)
└── Leaf API (extends Paper API — adds leaf config access)
└── Folia API (extends Paper API — replaces scheduler, adds region API)
A plugin targeting paper-api runs on Paper, Pufferfish, Purpur, and Leaf. A plugin targeting purpur-api only runs on Purpur and forks of Purpur. A plugin targeting folia-api and declaring folia-supported: true runs on Folia and Folia forks. A plugin using Folia's RegionScheduler instead of BukkitScheduler will compile against Paper-API but fail at runtime on any pre-Folia server because Server.getRegionScheduler() returns null there.
The current correct dependency for a new plugin in 2026:
<!-- Source: PaperMC documentation for plugin development -->
<!-- https://docs.papermc.io/paper/dev/getting-started/paper-plugins -->
<!-- pom.xml for a Paper-targeted plugin -->
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<!-- Target the Minecraft version you support -->
<version>1.21.5-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
Targeting paper-api gets you Paper, Pufferfish, Purpur, and Leaf compatibility with one dependency. The only reason to target spigot-api in 2026 is if you need your plugin to run on Spigot itself. Given Paper's 85-90% market share, that's a narrow use case.
The Decision You're Actually Making
When you pick a server jar, you're making a decision at three layers simultaneously: API compatibility (what plugins can you run), performance architecture (how does the server use your CPU), and operational stability (how maintained is this, who fixes bugs, how fast do updates ship).
Stability + maximum plugin compatibility: Paper. The hard fork from Spigot means Paper is now its own project with faster update cycles. The 1,600 patches covering async chunk loading, entity activation, hopper optimization, pathfinding stream removal, and the whole GC-friendly allocation reduction are already more than enough for most servers. Paper with Generational ZGC or well-tuned G1GC on Java 25 is the right base for the vast majority of servers.
Maximum performance, vanilla parity, single-tenant managed hosting: Pufferfish. DAB cuts entity brain tick cost dramatically. SIMD map rendering is free performance for image map users. Plugin compatibility with Paper is complete.
Configurability + Pufferfish performance: Purpur. One jar. All Pufferfish optimizations plus hundreds of gameplay configuration options. The villager lobotomize and lag threshold system alone justify it for servers with complex villager trading infrastructure.
Community-aggregated optimizations: Leaf. More patches, more risk, more performance headroom than Paper or Pufferfish for entity-heavy workloads. Worth evaluating if you're comfortable with a smaller maintenance team and want a single jar with broad community patches.
High player count, spread-out players, 16+ physical cores, willingness to rewrite plugin stack: Folia. Everything else first.
The tools to validate your choice: Spark shows you what's actually slow. The GC monitor shows you if your garbage collector is the bottleneck. The flame graph shows you if your plugins are the bottleneck. Make the decision with data, not with Discord recommendations.
The full chain from Bukkit's 2010 API design through Folia's regionized threading is fifteen years of the Java community building performance and flexibility on top of Mojang's single-threaded game loop. Every choice in that tree made sense given the constraints of its time. Understanding it tells you not just which jar to run, but why — and that understanding is what separates admins who fix problems from admins who copy flags until something accidentally works.