Altay
Plugins

Async tasks

The server is single threaded. Everything slow has to leave the tick.

Game logic runs on one thread. A handler that takes 300 ms freezes every player for 300 ms.

There is no clever way around this

Not a bigger server, not more cores, not a faster disk. The work has to leave the main thread.

What must never run in a handler

OperationWhy
HTTP requestsNetwork latency you do not control, measured in hundreds of milliseconds.
Remote database queriesSame problem, plus connection setup.
Large file reads and writesDisk stalls stop the tick as effectively as the network does.
Anything that sleeps or waits on a lockYou are asking the whole server to wait with you.

If you are unsure, time it. A tick has 50 ms in total, and the server has other things to do in it.

Scheduled tasks

For work that is cheap but repeated, use the scheduler. It still runs on the main thread, just not right now:

use pocketmine\scheduler\ClosureTask;

$this->getScheduler()->scheduleRepeatingTask(new ClosureTask(function() : void{
    // runs every 20 ticks, about once a second
}), 20);

scheduleDelayedTask() runs once, later. Tasks are cancelled automatically when the plugin is disabled.

Right tool for periodic cleanups, countdowns and saving state. Wrong tool for anything slow: "later on the main thread" is still on the main thread.

AsyncTask

For genuinely slow work, hand it to the worker pool:

use pocketmine\scheduler\AsyncTask;

class FetchTask extends AsyncTask{

    public function __construct(private string $url){}

    public function onRun() : void{
        $this->setResult(file_get_contents($this->url));
    }

    public function onCompletion() : void{
        $result = $this->getResult();
        // back on the main thread, safe to touch the server again
    }
}

$this->getServer()->getAsyncPool()->submitTask(new FetchTask("https://example.com/api"));

onRun() executes on a worker thread. onCompletion() runs back on the main thread once it finishes, which is the only place it is safe to talk to players, worlds or blocks.

The rules that actually bite

Do not carry objects across the boundary

Players, worlds, blocks and the server itself belong to the main thread. Pass a name or a UUID into the task and look the player up again in onCompletion().

The player may be gone. By the time your HTTP request returns, they have disconnected. Check before you use them:

$player = $this->getServer()->getPlayerExact($this->playerName);

if($player !== null && $player->isOnline()){
    $player->sendMessage("Done.");
}

Constructor arguments must survive serialisation. Scalars, arrays and strings cross safely. Closures and resources do not.

The pool is shared

Every plugin submits into the same workers. A task that blocks for thirty seconds delays everyone else's work, so this is not a free pass for unbounded waiting either.

Why PHP at all

PHP's threading model here is not the one most PHP developers know from the web: each thread gets its own isolated memory, and nothing is shared unless it was explicitly made shareable. That isolation is what makes the rules above absolute rather than advisory. Upstream's write up, Threading in PHP, is the longer version of this story.

On this page