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.

sequenceDiagram participant Main as Main code participant F as Fiber Main->>F: $fiber->start('978-0-00-000001-1') activate F F-->>Main: Fiber::suspend('waiting for database') deactivate F Note over Main: other work happens here Main->>F: $fiber->resume('Kindred') activate F Note over F: Fiber::suspend() returns 'Kindred' F-->>Main: Fiber::suspend('waiting again') deactivate F Main->>F: $fiber->resume() activate F F-->>Main: return 'KINDRED' (isTerminated() is true) deactivate F
<?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/await in 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.

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.

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).