Loading...
The VyroAI team · Published 12 Sep 2026 · 7 min read
Folia will not load your plugin until you say it is safe, and saying so is one line. Making it true is the real work: every scheduled task has to move to a scheduler that knows which part of the world it belongs to. This guide takes a normal Paper plugin through that change, step by step, and ends with one jar that runs on both Paper and Folia.
Paper runs the whole world on one main thread. Folia splits the world into regions, groups of nearby chunks, and ticks each region on its own thread, in parallel. There is no single main thread any more.
That breaks a basic assumption most plugins make: "if I schedule a sync task, it runs on the thread that owns everything." On Folia no thread owns everything. A task that touches a block has to run on the thread of the region that holds that block. A task that touches a player has to follow that player from region to region.
Folia's answer is strict. Its README says "only plugins that have been explicitly marked by the author(s) to work with Folia will be loaded." Unmarked plugins are skipped at startup.
Add one line to plugin.yml (or paper-plugin.yml):
name: MyPlugin
main: com.example.myplugin.MyPlugin
version: 1.0.0
api-version: '1.21'
folia-supported: true
PaperMC's own docs add the warning that matters: "This declaration alone is insufficient." The flag only gets your plugin past the gate. If your code still uses the old scheduler, it loads and then fails the first time a task runs. Paper ignores the flag, so it costs you nothing there.
Search your code for these. Each hit is something to change:
Bukkit.getScheduler() and getServer().getScheduler()new BukkitRunnable(), runTask, runTaskLater, runTaskTimerscheduleSyncDelayedTask, scheduleSyncRepeatingTaskcallSyncMethodplayer.teleport(...) and entity.teleport(...)The Paper docs are blunt about the first group: "The legacy BukkitScheduler is unsupported on Folia and should not be used."
Folia has four schedulers. Paper ships the same four, so code written against them compiles against the normal Paper API.
| Scheduler | Get it with | Use it for |
|---|---|---|
| `GlobalRegionScheduler` | `Bukkit.getGlobalRegionScheduler()` | Work tied to no place: broadcasts, world time, weather, a global countdown |
| `RegionScheduler` | `Bukkit.getRegionScheduler()` | Work on blocks at a location: placing, breaking, filling a chest |
| `EntityScheduler` | `entity.getScheduler()` | Work on one entity or player. The task follows it across regions |
| `AsyncScheduler` | `Bukkit.getAsyncScheduler()` | Work off the game threads: files, databases, web requests |
The methods follow one pattern. run runs on the next tick, runDelayed after a delay, and runAtFixedRate repeats. Delays are in ticks, except on the AsyncScheduler, which takes a TimeUnit. The entity versions take an extra retired callback, which runs if the entity is removed before your task does. They return null if the entity is already gone.
A quick way to decide: ask "what does this task touch?" A player means the entity scheduler. A block or a location means the region scheduler. Nothing in the world means global. No game objects at all means async.
If your plugin only has to run on current Paper and Folia, you can call the four schedulers directly everywhere, because Paper has them too. If you also want it to run on Spigot, put every task behind one small class that picks the right scheduler at runtime. Feature code then calls the adapter and never touches a scheduler itself.
public final class Schedulers {
private static final boolean FOLIA = detectFolia();
private static boolean detectFolia() {
try {
Class.forName("io.papermc.paper.threadedregions.RegionizedServer");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
public static boolean isFolia() { return FOLIA; }
/** Not tied to a place: broadcasts, time, global counters. */
public static void runGlobal(Plugin plugin, Runnable task) {
if (FOLIA) Bukkit.getGlobalRegionScheduler().execute(plugin, task);
else Bukkit.getScheduler().runTask(plugin, task);
}
/** Blocks at a location. */
public static void runAt(Plugin plugin, Location where, Runnable task) {
if (FOLIA) Bukkit.getRegionScheduler().execute(plugin, where, task);
else Bukkit.getScheduler().runTask(plugin, task);
}
/** One entity or player, after a delay in ticks. */
public static void runFor(Plugin plugin, Entity entity, Runnable task, long delayTicks) {
if (FOLIA) entity.getScheduler().execute(plugin, task, null, Math.max(1, delayTicks));
else Bukkit.getScheduler().runTaskLater(plugin, task, delayTicks);
}
/** Repeating global work, e.g. an autosave. */
public static void repeatGlobal(Plugin plugin, Runnable task, long delayTicks, long periodTicks) {
if (FOLIA) Bukkit.getGlobalRegionScheduler()
.runAtFixedRate(plugin, t -> task.run(), Math.max(1, delayTicks), periodTicks);
else Bukkit.getScheduler().runTaskTimer(plugin, task, delayTicks, periodTicks);
}
/** Off the game threads: files, databases, HTTP. */
public static void runAsync(Plugin plugin, Runnable task) {
if (FOLIA) Bukkit.getAsyncScheduler().runNow(plugin, t -> task.run());
else Bukkit.getScheduler().runTaskAsynchronously(plugin, task);
}
}
The Math.max(1, ...) keeps delays at one tick or more. That is the safe choice on the Folia schedulers.
The Class.forName check works on old and new builds alike. On recent Paper versions, the docs also show a brand check: ServerBuildInfo.buildInfo().isBrandCompatible(Key.key("papermc", "folia")).
Compile against the Paper API. You do not need a separate Folia dependency for any of this.
Swapping schedulers covers most of it. These are the rest:
teleportAsync(location) instead of teleport(location). Folia's README lists Entity#teleport as replaced.Bukkit.isOwnedByCurrentRegion(location) or Bukkit.isOwnedByCurrentRegion(entity). If it returns false, hand the work to the region or entity scheduler.HashMap that every task writes to is now a race. Use ConcurrentHashMap or keep the data per player.Thread.sleep, no database query and no web request on a region thread. Send them to the async scheduler and come back with the entity or region scheduler.Download Paper and Folia from papermc.io and run both locally with the same jar.
If you would rather describe the plugin than port it by hand, VyroAI.vip has a Paper + Folia platform option, labelled "Cross-compatible". When you pick it, the generator is told to write exactly what this guide describes: a scheduler adapter that detects Folia at runtime, folia-supported: true in plugin.yml, and a build against the Paper API. You get one jar for both servers, not two versions. You can also tick both Paper and Folia later in the project's Build settings. Test the result the same way as above.
Does folia-supported: true break my plugin on Paper? No. Paper ignores it. It only matters on Folia.
Do I need to add Folia as a dependency? No. The scheduler interfaces are in the Paper API. Compile against Paper.
Is there a Folia plugin template? Start from any Paper plugin template, add the flag from step 1 and the adapter from step 4. That is the whole difference.
Will Folia make my server faster? Only for the right server. Folia's README says servers that spread players out, like SMP and skyblock, gain the most, and recommends "at least 16 cores (not threads)". A small server with everyone at spawn gains little.