VyroAI

Loading...

How to make your plugin support Folia

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.

What Folia changes

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.

Step 1: Declare Folia support

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.

Step 2: Find every scheduler call

Search your code for these. Each hit is something to change:

The Paper docs are blunt about the first group: "The legacy BukkitScheduler is unsupported on Folia and should not be used."

Step 3: Pick the right scheduler for each task

Folia has four schedulers. Paper ships the same four, so code written against them compiles against the normal Paper API.

SchedulerGet it withUse 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.

Step 4: Write a scheduler adapter

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.

Step 5: Fix the calls that break on Folia

Swapping schedulers covers most of it. These are the rest:

Step 6: Test on both servers

Download Paper and Folia from papermc.io and run both locally with the same jar.

  1. Start each server and read the console. On Folia, a plugin without the flag is not loaded at all.
  2. Trigger every feature that schedules something: cooldowns, timers, delayed teleports, autosaves.
  3. Test with two players far apart. On Folia that puts them in different regions, which is where cross-region bugs appear.
  4. Test as a non-op. Op skips your own permission checks and hides bugs.

Doing it with VyroAI.vip

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.

FAQ

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.

Read next

All guides · Pricing