PHP Fibers: Async Programming That Reads Like Synchronous Code

PHP Fibers: Async Programming That Reads Like Synchronous Code

Last Updated: July 16, 2026By Tags: , , , , , , ,

What Problem Do Fibers Solve?

PHP has traditionally been a “one request, one thread” language. Your code runs top to bottom, waits for every database query and API call to complete, then returns a response. That simplicity is PHP’s greatest strength — but it’s also a bottleneck when you need to make three independent API calls that each take 200ms.

Sequentially, that’s 600ms of wall time. With Fibers, you can overlap those waits and finish in ~200ms. Not by running them in parallel on multiple CPU cores — Fibers are cooperative, single-threaded concurrency. They let your code “pause” while waiting for I/O and let another task run in the meantime.

Think of it like a chef who starts boiling pasta, then chops vegetables while the water heats up, instead of standing at the pot doing nothing.

The Basics: Start, Suspend, Resume

A Fiber is essentially a function that can pause itself and be resumed later. Here’s the fundamental API:

<?php

$fiber = new Fiber(function (): string {
    echo "Fiber: Starting work...\n";
    
    // Pause execution and send a value back to the caller
    $response = Fiber::suspend('waiting for data');
    
    echo "Fiber: Resumed with: $response\n";
    return 'done';
});

// Start the fiber — runs until the first suspend()
$suspendedValue = $fiber->start();
echo "Main: Fiber is paused. It said: $suspendedValue\n";

// Resume the fiber — passes a value back in
$result = $fiber->resume('here is your data');
echo "Main: Fiber returned: " . $fiber->getReturn() . "\n";

Output:

Fiber: Starting work...
Main: Fiber is paused. It said: waiting for data
Fiber: Resumed with: here is your data
Main: Fiber returned: done

Key methods: start() begins execution, Fiber::suspend() pauses from inside the fiber, resume() continues execution from outside the fiber, and getReturn() gets the fiber’s return value after it completes.

How Fibers Differ From Generators

If you’ve used PHP generators, fibers might seem redundant. They’re not. The critical difference is stack depth.

Generators can only yield from the generator function itself — not from a function it calls. Fibers can suspend() from anywhere in the call stack, including deeply nested function calls.

<?php

function deeplyNested(): void {
    // This works in a Fiber — you can suspend from ANY depth
    Fiber::suspend('paused from deep inside');
}

function middleLayer(): void {
    deeplyNested();
}

$fiber = new Fiber(function () {
    middleLayer(); // Suspension happens 3 levels deep
    return 'complete';
});

$value = $fiber->start();
echo $value; // 'paused from deep inside'
$fiber->resume();

This is why fibers are useful for real-world async code. You don’t need to restructure your entire call chain. A database driver can call Fiber::suspend() internally while waiting for a query result, and your application code doesn’t need to know or care.

Building a Simple Task Scheduler

Fibers alone don’t give you async I/O — they’re just pausable functions. To actually do concurrent work, you need a scheduler (event loop) that manages multiple fibers and decides when to resume them.

Here’s a minimal scheduler that demonstrates the concept:

<?php

class SimpleScheduler {
    /** @var SplQueue<Fiber> */
    private SplQueue $readyQueue;
    
    /** @var array<int, Fiber> */
    private array $waitingForTimer = [];

    public function __construct() {
        $this->readyQueue = new SplQueue();
    }

    public function schedule(Fiber $fiber): void {
        $this->readyQueue->enqueue($fiber);
    }

    public function run(): void {
        while (!$this->readyQueue->isEmpty() || !empty($this->waitingForTimer)) {
            // Check timers
            $now = hrtime(true);
            foreach ($this->waitingForTimer as $id => [$fiber, $wakeAt]) {
                if ($now >= $wakeAt) {
                    unset($this->waitingForTimer[$id]);
                    $this->readyQueue->enqueue($fiber);
                }
            }

            if ($this->readyQueue->isEmpty()) {
                usleep(1000); // Avoid busy-waiting
                continue;
            }

            $fiber = $this->readyQueue->dequeue();

            if (!$fiber->isStarted()) {
                $result = $fiber->start();
            } elseif ($fiber->isSuspended()) {
                $result = $fiber->resume();
            } else {
                continue;
            }

            // Handle the suspended value
            if ($fiber->isSuspended() && is_array($result) && ($result['type'] ?? '') === 'delay') {
                $wakeAt = hrtime(true) + ($result['ms'] * 1_000_000);
                $this->waitingForTimer[] = [$fiber, $wakeAt];
            } elseif ($fiber->isSuspended()) {
                $this->readyQueue->enqueue($fiber);
            }
        }
    }
}

function delay(int $ms): void {
    Fiber::suspend(['type' => 'delay', 'ms' => $ms]);
}

// Create tasks
$scheduler = new SimpleScheduler();

$scheduler->schedule(new Fiber(function () {
    echo "Task A: start\n";
    delay(100);
    echo "Task A: after 100ms\n";
    delay(100);
    echo "Task A: done\n";
}));

$scheduler->schedule(new Fiber(function () {
    echo "Task B: start\n";
    delay(50);
    echo "Task B: after 50ms\n";
    delay(150);
    echo "Task B: done\n";
}));

$scheduler->run();

Output (interleaved):

Task A: start
Task B: start
Task B: after 50ms
Task A: after 100ms
Task B: done
Task A: done

Both tasks run concurrently on a single thread. While Task A is delayed, Task B gets to run. This is cooperative multitasking in action.

Fibers in the Real World: Amp and Revolt

In production, you don’t write your own scheduler. Libraries like Amp v3 and its event loop library Revolt handle this for you, along with real non-blocking I/O (HTTP, database, file system).

<?php

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

// Fetch multiple URLs concurrently
$client = HttpClientBuilder::buildDefault();

$futures = [
    async(fn () => $client->request(new Request('https://api.example.com/users'))),
    async(fn () => $client->request(new Request('https://api.example.com/posts'))),
    async(fn () => $client->request(new Request('https://api.example.com/comments'))),
];

// All three requests run concurrently
$responses = await($futures);

foreach ($responses as $response) {
    echo $response->getStatus() . "\n";
}

The async() function wraps each callback in a Fiber. The await() function blocks until all futures complete. Under the hood, Revolt’s event loop suspends each fiber while waiting for its HTTP response and resumes it when data arrives.

Your code reads exactly like synchronous PHP. No callbacks, no promise chains, no ->then() nesting. That’s the entire point of fibers.

Common Mistakes

1. Confusing Concurrency With Parallelism

Fibers run on a single thread. They don’t use multiple CPU cores. If you have a CPU-intensive task (image processing, heavy computation), fibers won’t speed it up. They only help when your code spends time waiting — for I/O, network responses, or timers.

2. Using Fibers Without an Event Loop

Raw Fiber::suspend() pauses execution, but nothing will resume it unless you have a scheduler or event loop managing the fiber. A fiber that suspends without being resumed is just a paused function that never finishes.

3. Shared State Between Fibers

Since all fibers run in the same process, they share memory. Modifying a global variable from multiple fibers can cause race conditions — not the thread-level kind, but logical race conditions where the order of execution matters.

<?php
$counter = 0;

// Two fibers incrementing the same variable
// The result depends on execution order
$fiber1 = new Fiber(function () use (&$counter) {
    $temp = $counter;
    Fiber::suspend();  // Another fiber could run here
    $counter = $temp + 1;
});

$fiber2 = new Fiber(function () use (&$counter) {
    $counter++;
});

4. Catching FiberError

If you try to resume a fiber that’s already terminated, or start one that’s already running, PHP throws a FiberError. Always check $fiber->isSuspended() before calling resume().

Best Practices

  • Use Amp or Revolt in production. Don’t write your own event loop. These libraries handle edge cases around signal handling, timer resolution, and I/O multiplexing that you don’t want to deal with.
  • Keep fiber-aware code at the edges. Your business logic shouldn’t know it’s running inside a fiber. Let the framework handle suspension and resumption. If you’re calling Fiber::suspend() directly in application code, something is wrong.
  • Test for ordering bugs. Since fiber execution order isn’t guaranteed, write tests that exercise different interleavings. Tools like Amp’s test utilities can help simulate specific execution orders.
  • Profile before converting. Don’t fiber-ify code that isn’t I/O-bound. Sequential code is simpler to debug and maintain. Only introduce concurrency where you’ve measured a latency bottleneck from sequential I/O.
  • Consider FrankenPHP for web serving. FrankenPHP integrates with fibers at the web server level, allowing concurrent request handling within a single PHP worker. Worth evaluating if you’re building high-throughput APIs.

Wrapping Up

PHP Fibers give you cooperative multitasking that reads like synchronous code. They’re not threads, they’re not parallelism — they’re a structured way to say “I’m waiting, let someone else run” without callback chains or promise gymnastics.

For most developers, the right move is to use Amp or Revolt rather than working with raw fibers. These frameworks handle the event loop, provide async-aware HTTP clients and database drivers, and let you write concurrent PHP that looks and feels like the PHP you already know.

FAQ

Do I need Fibers for a typical Laravel app?

Probably not. Laravel’s request lifecycle is inherently sequential, and most bottlenecks are better solved with queues, caching, or database optimization. Fibers become useful when you’re making multiple external API calls per request and need to overlap their latency.

Can Fibers replace ReactPHP?

ReactPHP is evolving to use Fibers under the hood via Revolt (the shared event loop). You don’t need to choose — modern ReactPHP and Amp both use Revolt, and fibers are the execution mechanism. The ecosystem is converging.

Are Fibers available in PHP 8.1+?

Yes. Fibers were introduced in PHP 8.1 and are available in all subsequent versions. No extensions or special configuration needed — they’re part of the core language.

How do Fibers compare to Go goroutines?

Go goroutines are preemptively scheduled and can run on multiple OS threads (true parallelism). PHP Fibers are cooperatively scheduled and single-threaded (concurrency only). Go’s model is more powerful but also more complex. PHP’s model is simpler and sufficient for I/O-bound workloads, which is most of PHP’s use case.

editor's pick

latest video

news via inbox

Nulla turp dis cursus. Integer liberos  euismod pretium faucibua

Leave A Comment