PHP 8.1 Fibers Explained: Concurrency Without the Complexity

PHP 8.1 introduced Fibers, and they are the most significant change to PHP's execution model since generators. Unlike threads or async/await in other languages, Fibers do not add implicit concurrency. They are a tool for cooperative concurrency — a way for application code to yield control voluntarily and resume later. Understanding what Fibers actually are makes it much easier to use them correctly.

What Is a Fiber?

A Fiber is a stackful coroutine. The "stackful" part is key: when a Fiber suspends, its entire call stack is preserved. Any function that the Fiber called, and any function that function called, are all frozen in place. When the Fiber resumes, execution continues exactly where it left off, with the entire call stack intact.

This is different from generators, which are stackless: a generator can only yield from the generator function itself, not from a function it calls. Fibers can suspend from anywhere in their call stack, which makes them genuinely useful for wrapping existing synchronous code in async contexts.

Fibers have four states:

  • Created — the Fiber has been instantiated but not started
  • Running — the Fiber is currently executing
  • Suspended — the Fiber has called Fiber::suspend()
  • Terminated — the Fiber's callback has returned

The Core API

Creating and Starting a Fiber

$fiber = new Fiber(function (): void {
    $value = Fiber::suspend('first suspension');
    echo "Resumed with: {$value}\n";

    Fiber::suspend('second suspension');
    echo "Resumed again\n";
});

$result1 = $fiber->start();          // starts the fiber, runs until first suspend
echo "Fiber suspended with: {$result1}\n"; // 'first suspension'

$result2 = $fiber->resume('hello'); // resumes, passes 'hello' as return of suspend()
echo "Fiber suspended with: {$result2}\n"; // 'second suspension'

$fiber->resume();                    // resumes final time, fiber completes
var_dump($fiber->isTerminated());    // true

Output:

Fiber suspended with: first suspension
Resumed with: hello
Fiber suspended with: second suspension
Resumed again

Fiber::start() and resume()

$fiber->start(...$args) starts the Fiber and passes arguments to the Fiber's callable. It runs until the Fiber suspends or terminates, then returns the value passed to Fiber::suspend() (or null if the Fiber terminated).

$fiber->resume($value) resumes a suspended Fiber. The $value becomes the return value of the Fiber::suspend() call inside the Fiber. It runs until the next suspension or termination.

Fiber::getReturn()

$fiber = new Fiber(function (): int {
    Fiber::suspend();
    return 42;
});

$fiber->start();
$fiber->resume();

echo $fiber->getReturn(); // 42

getReturn() throws a FiberError if called before the Fiber has terminated, so check $fiber->isTerminated() first, or call it only after you know the Fiber is done.

Throwing Exceptions into a Fiber

$fiber = new Fiber(function (): void {
    try {
        Fiber::suspend();
    } catch (\RuntimeException $e) {
        echo "Caught: {$e->getMessage()}\n";
    }
});

$fiber->start();
$fiber->throw(new \RuntimeException('something went wrong'));
// Output: Caught: something went wrong

Fibers vs Generators: A Clear Distinction

Generators look similar but are fundamentally different:

FeatureGeneratorFiber
Stack preserved on suspendNo (stackless)Yes (stackful)
Can yield from called functionsNoYes
Bidirectional communicationYes (via send())Yes (via resume())
Implements IteratorYesNo
Use caseLazy sequencesCooperative concurrency

A generator can only yield from within the generator function itself. If a generator calls a helper function, that helper function cannot yield. A Fiber can suspend from any depth in its call stack, which is what makes them suitable for async I/O wrappers.

Real Use Case: Concurrent HTTP Requests with curl_multi

Fibers shine when you need to interleave I/O operations without threads. Here is a simplified event-loop-like pattern using curl_multi and Fibers:

function fetchUrl(string $url): string {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 10,
    ]);

    $mh = curl_multi_init();
    curl_multi_add_handle($mh, $ch);

    // Poll until complete, suspending the Fiber on each iteration
    do {
        $status = curl_multi_exec($mh, $running);
        if ($running) {
            // Suspend: yield control back to the event loop
            Fiber::suspend();
        }
    } while ($running && $status === CURLM_OK);

    $body = curl_multi_getcontent($ch);
    curl_multi_remove_handle($mh, $ch);
    curl_multi_close($mh);

    return $body;
}

// Create fibers for concurrent requests
$urls = [
    'https://api.example.com/users',
    'https://api.example.com/posts',
    'https://api.example.com/comments',
];

$fibers = array_map(
    fn(string $url) => new Fiber(fn() => fetchUrl($url)),
    $urls
);

// Start all fibers
foreach ($fibers as $fiber) {
    $fiber->start();
}

// Run the event loop until all fibers are done
while (true) {
    $allDone = true;
    foreach ($fibers as $fiber) {
        if (!$fiber->isTerminated()) {
            $allDone = false;
            if ($fiber->isSuspended()) {
                $fiber->resume();
            }
        }
    }
    if ($allDone) break;
}

// Collect results
foreach ($fibers as $i => $fiber) {
    echo "URL {$i}: " . strlen($fiber->getReturn()) . " bytes\n";
}

This is illustrative — a production implementation would use a proper event loop library. But it demonstrates the mechanics: multiple Fibers run interleaved, each yielding control while waiting for network I/O, and the outer loop drives them all forward.

How Revolt and ReactPHP Use Fibers

Revolt is the PHP event loop standard that emerged from a collaboration between the ReactPHP and Amp teams. In Revolt's model, Fibers are the primitive for writing async code that looks synchronous.

use Revolt\EventLoop;

// Revolt uses EventLoop::defer() to queue callbacks for the next event loop tick.
// Each deferred callback runs inside a Fiber. Any async I/O library built on Revolt
// will call Fiber::suspend() internally when waiting for I/O readiness.
EventLoop::defer(function () {
    echo "First callback — runs in its own Fiber\n";
});

EventLoop::defer(function () {
    echo "Second callback — interleaved with the first\n";
});

EventLoop::run(); // drives the event loop until no pending callbacks remain

Under the hood, Revolt wraps each deferred callback in a Fiber. When async I/O is pending, the Fiber calls Fiber::suspend(), passing the I/O handle to the event loop. The event loop uses epoll or kqueue to wait for I/O readiness, then resumes the appropriate Fiber with the result. The application code sees a simple function call — no callbacks, no promises, no async/await keywords.

Amp v3 is built on Revolt and uses this pattern pervasively:

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\Future\await;

$client = HttpClientBuilder::buildDefault();

// async() runs the callback in a new Fiber — returns a Future immediately.
// The two HTTP requests are initiated concurrently:
$future1 = async(fn() => $client->request(new Request('https://api.example.com/users')));
$future2 = async(fn() => $client->request(new Request('https://api.example.com/posts')));

// await() blocks the current Fiber until both futures resolve:
[$response1, $response2] = await([$future1, $future2]);

Fibers Are Not Threads

This is the most important thing to understand about Fibers. They are not threads. There is no parallelism. At any given moment, exactly one Fiber is running. A Fiber only suspends when it explicitly calls Fiber::suspend(). If a Fiber runs a CPU-intensive loop without suspending, it blocks all other Fibers until it completes.

There is no shared memory concern between Fibers because they run in the same PHP process and share the same heap. This is also why there are no race conditions — you never have two Fibers running simultaneously.

The concurrency benefit comes from I/O. When a Fiber is waiting for a network response or a database query, it can suspend and let other Fibers do their work. For I/O-bound tasks, this can be a significant improvement. For CPU-bound tasks, Fibers provide no benefit.

Common Mistakes

Calling Fiber::suspend() Outside a Fiber

// This throws FiberError: Cannot call Fiber::suspend() when not in a fiber
Fiber::suspend();

Always check Fiber::getCurrent() !== null before calling Fiber::suspend() in library code that might be called both inside and outside a Fiber context.

Forgetting That resume() Can Throw

If an exception propagates out of a Fiber (not caught inside it), the exception is rethrown at the call site of start() or resume(). Always wrap Fiber driving code in try/catch:

try {
    $fiber->resume();
} catch (\Throwable $e) {
    // Handle exception from inside the Fiber
    error_log($e->getMessage());
}

Blocking Operations Inside Event Loops

In an event-loop context, any blocking call — sleep(), synchronous file_get_contents(), a blocking PDO query — stalls the entire event loop. All Fibers are blocked until that call returns. In event-loop-based code, use non-blocking I/O wrappers provided by the framework.

Fibers are a low-level primitive. In day-to-day PHP code you will typically interact with them through Revolt-based libraries (Amp, ReactPHP) rather than creating Fibers directly. But understanding the mechanics makes debugging async PHP significantly easier.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Please share this article on your favorite website or platform.