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
| Operation | Why |
|---|---|
| HTTP requests | Network latency you do not control, measured in hundreds of milliseconds. |
| Remote database queries | Same problem, plus connection setup. |
| Large file reads and writes | Disk stalls stop the tick as effectively as the network does. |
| Anything that sleeps or waits on a lock | You 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.