Fibers and Asynchronous PHP
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against PHP 8.5, checked on 2026-10-05. PHP executes one request with one thread, top to bottom, and most PHP code never needs anything else: a web request is short, and the server runs many requests in parallel processes. Some programs, however, wait on many things at once — dozens of HTTP calls, a WebSocket server, a queue consumer that also serves metrics. Fibers (8.1) are the low-level building block that lets such programs pause one task while another runs, without callback pyramids or promise chains.
What a fiber is
A Fiber is a function with its own call stack that can suspend itself at any depth of nested calls and be
resumed later from the outside, exactly where it stopped. Values flow both ways: Fiber::suspend($value) hands a
value to the code that started or resumed the fiber, and $fiber->resume($value) makes that value the return value of
Fiber::suspend() inside the fiber.
<?php
declare(strict_types=1);
$fiber = new Fiber(function (string $isbn): string {
echo "fiber: looking up {$isbn}\n";
$title = Fiber::suspend('waiting for database'); // pause; main code continues
echo "fiber: resumed with {$title}\n";
Fiber::suspend('waiting again');
return strtoupper($title);
});
$status = $fiber->start('978-0-00-000001-1'); // runs until the first suspend
echo "main: fiber suspended ({$status})\n";
$status = $fiber->resume('Kindred'); // value becomes Fiber::suspend()'s return value
echo "main: ({$status})\n";
$fiber->resume();
var_dump($fiber->isTerminated(), $fiber->getReturn()); // bool(true) string(7) "KINDRED"
try {
$fiber->resume();
} catch (FiberError $e) {
echo $e->getMessage(), "\n"; // Cannot resume a fiber that is not suspended
}
fiber: looking up 978-0-00-000001-1
main: fiber suspended (waiting for database)
fiber: resumed with Kindred
main: (waiting again)
bool(true)
string(7) "KINDRED"
Cannot resume a fiber that is not suspended
The API is small: start(...$args), resume($value), throw(Throwable $e) (raise an exception at the suspension
point), getReturn(), the state checks isStarted(), isSuspended(), isRunning() and isTerminated(), and the
static Fiber::suspend() and Fiber::getCurrent(). Calling a method in the wrong state throws FiberError.
Source: Fibers and The Fiber class
What fibers are not
-
Not threads: only one fiber runs at a time, in the same thread. Nothing runs in parallel, there are no data races, and a CPU-bound loop inside a fiber blocks everything else.
-
Not preemptive: a fiber runs until it chooses to suspend (cooperative multitasking).
-
Not automatic async I/O: calling
file_get_contents()or a PDO query inside a fiber still blocks the whole process. Concurrency needs non-blocking I/O (stream_select(), non-blocking sockets,curl_multi) and an event loop that resumes each fiber when its I/O is ready. -
Not "coloured": unlike
async/awaitin JavaScript or C#, a function that suspends looks like any other function, so libraries can offer synchronous-looking APIs that are asynchronous underneath.
A minimal scheduler
The following round-robin scheduler shows the idea behind event loops: each task is a fiber; a task that has to wait suspends itself, and the scheduler resumes the next one. Real loops wait on sockets and timers instead of counting ticks.
<?php
declare(strict_types=1);
final class Scheduler
{
/** @var SplQueue<Fiber> */
private SplQueue $ready;
public function __construct() { $this->ready = new SplQueue(); }
public function spawn(callable $task): void
{
$this->ready->enqueue(new Fiber($task));
}
public function run(): void
{
while (!$this->ready->isEmpty()) {
$fiber = $this->ready->dequeue();
$fiber->isStarted() ? $fiber->resume() : $fiber->start();
if (!$fiber->isTerminated()) {
$this->ready->enqueue($fiber); // not finished: back of the queue
}
}
}
}
/** "Waits" for an I/O operation by yielding control $ticks times. */
function wait(int $ticks): void
{
for ($i = 0; $i < $ticks; $i++) {
Fiber::suspend();
}
}
$scheduler = new Scheduler();
foreach (['Kindred' => 2, 'Solaris' => 1, 'Dawn' => 3] as $title => $latency) {
$scheduler->spawn(function () use ($title, $latency): void {
echo "request {$title}\n";
wait($latency); // other tasks run meanwhile
echo "response {$title}\n";
});
}
$scheduler->run();
request Kindred
request Solaris
request Dawn
response Solaris
response Kindred
response Dawn
The three "requests" overlap: the total time is that of the slowest, not the sum. This is what event-loop libraries do with real I/O.
Source: Fibers
The event-loop ecosystem
Fibers were added so that libraries could share a common foundation; application code normally uses these libraries
rather than Fiber directly:
-
Revolt is the shared event loop (
Revolt\EventLoop) built on fibers, used by AMPHP and ReactPHP. -
AMPHP provides fiber-based asynchronous HTTP clients and servers, database drivers (MySQL, PostgreSQL, Redis), WebSockets, parallel processing and synchronisation primitives, with synchronous-looking APIs.
-
ReactPHP is the older, promise-based event-loop ecosystem, now interoperable with fibers via Revolt.
-
Swoole and OpenSwoole are C extensions providing their own coroutine runtime and servers; they are not built on PHP’s
Fiber.
In web applications, asynchronous concurrency usually comes from the server instead: FrankenPHP’s worker mode,
RoadRunner and Swoole keep the application in memory between requests, and Laravel Octane builds on them (see
The PHP Request Model and Servers and
Octane and Performance). Within a single request, fanning out HTTP
calls with curl_multi or Laravel’s Http::pool() gives most of the benefit with no event loop at all.
Process-level concurrency: pcntl and posix
On Unix-like systems, the CLI can run work in parallel by forking processes. The pcntl extension (CLI only, not
available on Windows) provides pcntl_fork(), pcntl_waitpid(), signal handling and pcntl_async_signals(); posix
exposes process ids, users and groups. Queue workers use signals for graceful shutdown.
<?php
declare(strict_types=1);
// Graceful shutdown of a long-running worker (run with the CLI; needs ext-pcntl)
pcntl_async_signals(true);
$running = true;
pcntl_signal(SIGTERM, function () use (&$running): void {
$running = false; // finish the current job, then stop
});
$jobs = ['send reminder 1', 'send reminder 2', 'send reminder 3'];
foreach ($jobs as $i => $job) {
if (!$running) {
break;
}
echo "processing {$job}\n";
if ($i === 0) {
posix_kill(posix_getpid(), SIGTERM); // simulate "systemctl stop"
}
}
echo "worker stopped cleanly\n";
// Fork a child process and wait for it
$pid = pcntl_fork();
if ($pid === 0) {
echo "child: rebuilding search index\n";
exit(0);
}
pcntl_waitpid($pid, $status);
echo 'parent: child exited with ', pcntl_wexitstatus($status), "\n";
processing send reminder 1
worker stopped cleanly
child: rebuilding search index
parent: child exited with 0
For portable process control, proc_open() and Symfony Process (Laravel’s Process facade) start and supervise
child processes; see Command-Line PHP.
Source: Process Control (pcntl), pcntl_signal and POSIX
The polling API (upcoming in PHP 8.6)
Event loops need to ask the operating system "which of these sockets are ready?". Until now PHP only offered
stream_select(), which does not scale to many connections. PHP 8.6 adds a native polling API in the Io\Poll
namespace — a Io\Poll\Context with Io\Poll\Watcher objects for stream handles (StreamPollHandle), backed by the
best mechanism the platform offers (selected through the Io\Poll\Backend enum: epoll, kqueue, …). It is aimed at
event-loop authors such as Revolt and AMPHP, which can then drop their own extension-based backends; application code
will benefit indirectly.
Source: RFC: Polling API and stream_select
Where async fits in a shared-nothing language
PHP’s classic model — a fresh process state per request, everything freed at the end — is what makes it robust and easy to scale horizontally. Asynchronous code changes that trade-off:
-
Long-running processes (event loops, worker mode) must avoid state leaks between units of work: static properties, singletons and global caches survive.
-
Most libraries and extensions (PDO, most HTTP clients) are blocking; mixing them into an event loop stalls it.
-
Errors in one task can affect others sharing the process.
Use fibers and event loops for I/O-heavy long-running services (WebSocket servers, crawlers, proxies, high-fan-out API gateways); keep request/response web applications on the shared-nothing model, adding queues for slow work (see Queues and Jobs).
Where older books and tutorials are outdated
None of the books consulted for this section covers fibers (8.1). Older material describes asynchronous PHP through
generator-based coroutines and promise libraries (early AMPHP, ReactPHP) or the Swoole extension; Revolt and the
fiber-based AMPHP v3 replaced the generator approach. pcntl_signal_dispatch() polling has been superseded by
pcntl_async_signals(true) (7.1).