Async PHP is Here: A Practical Guide to Fibers

Async PHP is Here: A Practical Guide to Fibers

Remember callback hell? Remember chaining endless .then() calls? It’s time to forget them. PHP Fibers are here, and they’ve changed everything.

Introduction: The Old World of PHP Concurrency

For the longest time, PHP’s approach to I/O was simple: you ask for something (a database query, an API call), and you wait. Your entire process would just sit there, blocked, doing nothing until the response came back. It was simple, but horribly inefficient.

We tried to fix it. We used callbacks, which led to the infamous “callback hell.” We moved on to Promises (thank you, ReactPHP and Amp!), which were a huge improvement but still required a different way of thinking, chaining .then() and handling errors in a less-than-intuitive way.

Then came PHP 8.1, and with it, Fibers. Fibers are a low-level primitive for cooperative multitasking. In simple terms, they let us write asynchronous code that looks and feels synchronous. Let’s dive in.

What Exactly is a Fiber?

A Fiber is essentially a function that can be paused and resumed.

Think of it like this: when you call a regular function, it runs until it returns a value. It can’t be interrupted. A Fiber, on the other hand, can be started, run for a bit, and then say, “Okay, I’m waiting for something, I’ll pause myself now.” While it’s paused, other code can run. When the thing it was waiting for is ready, you can resume the Fiber, and it picks up right where it left off, with its entire stack (local variables and all) intact.

The key here is “cooperative.” A Fiber decides when to pause itself using Fiber::suspend(). This is different from preemptive multitasking (like threads), where the operating system can interrupt things whenever it wants.

The Catch: Fibers Need a Manager

A Fiber on its own is just a pausable function. It’s powerful, but it doesn’t know when to resume. To make Fibers useful for I/O, you need two more things:

  1. An Event Loop: A loop that constantly checks for I/O events (like “this socket is now readable”).
  2. A Scheduler: Code that decides which Fiber to run next, and resumes a paused Fiber when its I/O event occurs.

You could write these yourself, but you really shouldn’t. This is where libraries like Amp and Revolt come in. They provide robust, production-ready event loops that are now built on top of Fibers.

A Practical Example: Concurrent HTTP Requests

Let’s see what this looks like in practice. We want to fetch data from two different APIs and we don’t want to wait for the first one to finish before starting the second.

We’ll use amphp/http-client, a powerful async client that uses Fibers under the hood.

First, you’ll need to install the necessary packages:

composer require amphp/http-client revolt/event-loop

Now, look at the code. This will all run inside a single PHP script.

<?php

require_once __DIR__ . '/vendor/autoload.php';

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;

// This function will be run inside a Fiber by the event loop
function fetchUrl(string $url): mixed
{
    try {
        // 1. Create a new async HTTP client
        $client = HttpClientBuilder::buildDefault();

        // 2. Make the request. This returns a "Future" immediately.
        $response = $client->request(new Request($url));

        // 3. Await the result. THIS is where the magic happens.
        // The Fiber suspends here, allowing other Fibers to run.
        $body = $response->getBody()->buffer();

        echo "Finished fetching $url\n";
        return json_decode($body, true);

    } catch (\Exception $e) {
        echo "Error fetching $url: " . $e->getMessage() . "\n";
        return null;
    }
}

// The main execution block
Revolt\EventLoop::run(function () {
    $start = microtime(true);

    echo "Starting requests...\n";

    // Create two "Futures" by calling our function.
    // The event loop will schedule these to run concurrently.
    $future1 = Revolt\EventLoop::Future->map(fn() => fetchUrl('https://api.github.com/users/amphp'));
    $future2 = Revolt\EventLoop::Future->map(fn() => fetchUrl('https://api.github.com/users/revoltphp'));

    // Await both futures to complete.
    // The event loop manages suspending/resuming the Fibers until both are done.
    [$githubUser1, $githubUser2] = Revolt\EventLoop\Future\all([$future1, $future2]);

    $end = microtime(true);

    printf("Fetched user '%s' and '%s'\n", $githubUser1['login'] ?? 'n/a', $githubUser2['login'] ?? 'n/a');
    printf("Total time: %.2f seconds\n", $end - $start);
});

What’s Happening Here?

  1. We define a function fetchUrl that performs an HTTP request.
  2. Inside Revolt\EventLoop::run(), we schedule this function to be called twice, concurrently. The event loop wraps each call in a Fiber.
  3. When $response->getBody()->buffer() is called, the HTTP client knows the data isn’t there yet. It tells the event loop, “Hey, I’m waiting for data on this socket,” and then suspends the Fiber.
  4. The event loop, now free, can work on the other Fiber, starting the second HTTP request. That one also suspends.
  5. The event loop now waits. When data arrives on the first socket, the loop resumes the first Fiber, which continues execution. The same happens for the second.

If each request takes 1 second, the total time will be around 1 second, not 2. That’s the power of non-blocking I/O. And notice how clean the code is—no callbacks, no .then(). It reads like a synchronous script.

Fibers vs. The Old Guard

  • vs. Generators: Generators were a clever hack to simulate this, but they were clunky. Fibers are a dedicated language feature with a proper API and their own stack.
  • vs. Promises: Promises are still relevant! In fact, libraries like Amp often use them internally. But Fibers allow us to build APIs that consume those Promises without forcing the messy chain syntax on the end user.
  • vs. Swoole: Swoole is a PHP extension that offers true parallelism with threads and a high-performance, built-in event loop. It’s incredibly powerful but adds a layer of complexity to your stack. Fiber-based async is pure PHP, running on a standard FPM or CLI process.

Conclusion: A New Era for PHP

Fibers are not a silver bullet. They introduce new challenges in debugging and require a shift in thinking about “invisible concurrency” (a function that looks synchronous might pause your code).

However, they are a massive leap forward. By providing a low-level, standardized way to handle cooperative multitasking, they have enabled a new generation of user-friendly async libraries. For I/O-heavy applications, this means more performance, less waiting, and cleaner code. It’s a good time to be a PHP developer.


Have you experimented with Fibers? What are your favorite async libraries? Let me know on Twitter or LinkedIn.

Related Reading: