Fibers (PHP 8.1+) — cooperative multitasking, suspend/resume
Concept
Fibers (PHP 8.1) are a low-level primitive for cooperative multitasking — the ability to pause execution inside a function and resume it later from the outside, without threads or forking. A Fiber wraps a callable. When you call Fiber::suspend($value), the fiber's stack is paused and control returns to the caller with whatever value was suspended. The caller can later call $fiber->resume($value) to continue the fiber from where it left off, optionally injecting a value back into the suspend() call.
Fibers are not coroutines in the Node.js async/await sense — they do not parallelize I/O automatically. PHP still executes on a single thread per request by default. What Fibers provide is a structured way to interleave execution without callback hell. Event loops (ReactPHP, Revolt, Swoole) use Fibers to implement non-blocking I/O: when a network read blocks, the event loop suspends the fiber and services other fibers, resuming when data arrives. This is identical in concept to JavaScript's event loop.
Understanding Fiber internals: a Fiber has its own call stack, separate from the main stack. The PHP VM maintains a list of "fiber contexts". Suspending a fiber saves the current call stack pointer and register state; resuming it restores them. Memory-wise each fiber needs its own stack allocation (default 8 MB, configurable with --with-fiber-stack-size). Creating thousands of fibers is feasible; creating millions is not.
The practical interface for application developers is not raw Fibers but async libraries built on them. ReactPHP uses revolt/event-loop which in turn uses Fibers to make async code look synchronous. Spatie's async package uses Fibers (or child processes). Laravel's Http::async() in Laravel 11+ leverages Fibers under the hood via Guzzle + ReactPHP to fire multiple HTTP requests concurrently within a single PHP process.
A critical gotcha: Fiber::suspend() can only be called from within a running fiber's call stack. Calling it from the main fiber (i.e., normal PHP execution) throws an FiberError. You cannot suspend across a C extension boundary either — for example, you cannot suspend inside a PDO callback. These limitations mean Fibers are an infrastructure primitive, not something most application code touches directly.
Code Example
<?php
declare(strict_types=1);
// Basic Fiber: suspend/resume with value passing in both directions
$fiber = new Fiber(function (): void {
// The value passed to $fiber->start() arrives here
$valueFromCaller = Fiber::suspend('first suspension');
echo "Fiber received from caller: {$valueFromCaller}\n";
$valueFromCaller2 = Fiber::suspend('second suspension');
echo "Fiber received second value: {$valueFromCaller2}\n";
});
// Start the fiber — runs until the first suspend()
$suspendedValue = $fiber->start();
echo "Fiber suspended with: {$suspendedValue}\n"; // first suspension
// Resume the fiber, injecting a value into suspend()'s return
$suspendedValue2 = $fiber->resume('hello from main');
echo "Fiber suspended with: {$suspendedValue2}\n"; // second suspension
$fiber->resume('goodbye');
// Fiber is now terminated
echo "Fiber terminated: " . ($fiber->isTerminated() ? 'yes' : 'no') . "\n";
// -------------------------------------------------------------------------
// Practical example: a simple round-robin scheduler for concurrent tasks
// -------------------------------------------------------------------------
final class SimpleScheduler
{
/** @var \SplQueue<Fiber> */
private \SplQueue $queue;
public function __construct()
{
$this->queue = new \SplQueue();
}
public function add(callable $task): void
{
$this->queue->enqueue(new Fiber($task));
}
public function run(): void
{
// Seed: start all fibers once
$active = [];
while (!$this->queue->isEmpty()) {
$fiber = $this->queue->dequeue();
$fiber->start();
if (!$fiber->isTerminated()) {
$active[] = $fiber;
}
}
// Round-robin resume until all fibers complete
while (!empty($active)) {
foreach ($active as $key => $fiber) {
$fiber->resume();
if ($fiber->isTerminated()) {
unset($active[$key]);
}
}
$active = array_values($active);
}
}
}
$scheduler = new SimpleScheduler();
$scheduler->add(function (): void {
echo "Task A: step 1\n";
Fiber::suspend();
echo "Task A: step 2\n";
Fiber::suspend();
echo "Task A: done\n";
});
$scheduler->add(function (): void {
echo "Task B: step 1\n";
Fiber::suspend();
echo "Task B: done\n";
});
$scheduler->run();
// Output:
// Task A: step 1
// Task B: step 1
// Task A: step 2
// Task B: done
// Task A: doneInterview Q&A
Q: How do Fibers differ from traditional PHP generators, and which should you prefer?
Generators (since PHP 5.5) use yield to produce values lazily and can receive values via Generator::send(). They are essentially a single-direction coroutine tied to the Iterator interface. Fibers are a more general primitive: they have their own full call stack (a generator's yield can only suspend the innermost generator, not a deeply nested call), they can pass values in both directions at suspension points, and they are designed to integrate with event loops. A generator cannot suspend execution inside a nested function call — Fibers can. The practical takeaway: use generators for lazy iteration over sequences; use Fibers (usually via an async library like ReactPHP or revolt/event-loop) when you need cooperative multitasking or non-blocking I/O within a single PHP process.
Q: What happens to memory when you create large numbers of Fibers?
Each Fiber allocates its own call stack. On most systems the default Fiber stack size is 8 MB. Creating 100 concurrent Fibers could therefore consume 800 MB in stack space alone — even if each Fiber uses only a few KB of actual stack depth, the virtual memory reservation is made upfront. Production event loops like Revolt mitigate this by pooling and recycling Fiber instances rather than creating new ones per task. When implementing your own Fiber-based concurrency, always bound the maximum number of concurrent Fibers and consider using WeakMap or WeakReference to allow GC of completed Fiber results rather than retaining them in long-lived arrays.
Q: Can Fibers make a blocking PDO or file-system call non-blocking?
No. PHP Fibers are a cooperative scheduling primitive, not an OS-level async I/O mechanism. Calling $pdo->query('SELECT ...') inside a Fiber still blocks the entire PHP process for the duration of the query — you cannot Fiber::suspend() across a C extension call boundary. True non-blocking database access requires a dedicated async driver (e.g., amphp/mysql, ReactPHP/mysql) that uses non-blocking sockets and integrates with an event loop. What Fibers enable is that once the async driver detects the socket is readable, it can resume the correct Fiber via the event loop, making the code look synchronous from the application's perspective even though the underlying I/O is event-driven.