PHP 8.1 introduced Fibers as a foundation for cooperative multitasking. Most developers use them only indirectly through ReactPHP or Amp. But understanding Fibers at the mechanism level unlocks the ability to build your own concurrency primitives. I show how Fibers work, implement a minimal cooperative scheduler, and use it to run parallel HTTP requests without a framework dependency.
What is a Fiber?
<?php
declare(strict_types=1);
// A Fiber is a lightweight coroutine - it can be suspended and resumed
// Unlike threads, only one Fiber runs at a time (cooperative, not preemptive)
$fiber = new Fiber(function(): string {
echo "Fiber: step 1\n";
$received = Fiber::suspend('from fiber: first pause'); // pause here
echo "Fiber: resumed with: {$received}\n";
Fiber::suspend('from fiber: second pause');
echo "Fiber: finishing\n";
return 'fiber result';
});
// Start the fiber - runs until first Fiber::suspend()
$value1 = $fiber->start();
echo "Main: fiber paused, got: {$value1}\n"; // 'from fiber: first pause'
// Resume the fiber - runs until next Fiber::suspend() or return
$value2 = $fiber->resume('hello from main');
echo "Main: fiber paused again, got: {$value2}\n"; // 'from fiber: second pause'
// Resume to completion
$fiber->resume('final resume');
echo "Main: fiber terminated, returned: " . $fiber->getReturn() . "\n";
// Output:
// Fiber: step 1
// Main: fiber paused, got: from fiber: first pause
// Fiber: resumed with: hello from main
// Main: fiber paused again, got: from fiber: second pause
// Fiber: finishing
// Main: fiber terminated, returned: fiber result
Building a minimal cooperative scheduler
<?php
declare(strict_types=1);
class CooperativeScheduler
{
/** @var Fiber[] */
private array $queue = [];
private array $callbacks = []; // fiber => callback when done
// Add a task to the scheduler
public function add(callable $task, ?callable $onDone = null): void
{
$fiber = new Fiber($task);
$this->queue[] = $fiber;
if ($onDone !== null) {
$this->callbacks[spl_object_id($fiber)] = $onDone;
}
}
// Run until all tasks complete
public function run(): void
{
// Start all fibers
$started = [];
foreach ($this->queue as $fiber) {
$fiber->start();
if (!$fiber->isTerminated()) {
$started[] = $fiber;
} elseif ($cb = $this->callbacks[spl_object_id($fiber)] ?? null) {
$cb($fiber->getReturn());
}
}
$this->queue = $started;
// Round-robin until all finish
while (!empty($this->queue)) {
$stillRunning = [];
foreach ($this->queue as $fiber) {
if ($fiber->isSuspended()) {
$fiber->resume();
}
if (!$fiber->isTerminated()) {
$stillRunning[] = $fiber;
} elseif ($cb = $this->callbacks[spl_object_id($fiber)] ?? null) {
$cb($fiber->getReturn());
}
}
$this->queue = $stillRunning;
}
}
}
// Usage
$scheduler = new CooperativeScheduler();
$results = [];
for ($i = 1; $i <= 5; $i++) {
$scheduler->add(
function() use ($i): string {
echo "Task {$i}: starting\n";
Fiber::suspend(); // yield control back to scheduler
echo "Task {$i}: continuing\n";
Fiber::suspend(); // yield again
echo "Task {$i}: finishing\n";
return "result_{$i}";
},
function(string $result) use (&$results): void {
$results[] = $result;
}
);
}
$scheduler->run();
print_r($results); // ['result_1', 'result_2', ..., 'result_5']
Parallel HTTP requests with a socket-based scheduler
<?php
declare(strict_types=1);
// Non-blocking HTTP using sockets + Fibers
// Conceptually what ReactPHP does internally
class AsyncHttpClient
{
private array $pendingSockets = [];
public function fetchAsync(string $url): mixed
{
$parsed = parse_url($url);
$host = $parsed['host'];
$path = $parsed['path'] ?? '/';
$port = $parsed['port'] ?? ($parsed['scheme'] === 'https' ? 443 : 80);
// Open non-blocking socket
$socket = fsockopen(
($parsed['scheme'] === 'https' ? 'ssl://' : '') . $host,
$port,
$errno, $errstr, 10
);
stream_set_blocking($socket, false);
// Send HTTP request
$request = "GET {$path} HTTP/1.1\r\n"
. "Host: {$host}\r\n"
. "Connection: close\r\n\r\n";
fwrite($socket, $request);
// Register socket with current fiber
$this->pendingSockets[spl_object_id(Fiber::getCurrent())] = $socket;
// Suspend: yield control back to scheduler
// Scheduler will resume us when socket has data
return Fiber::suspend($socket);
}
public function readResponse($socket): string
{
$response = '';
while (!feof($socket)) {
$chunk = fread($socket, 8192);
if ($chunk === false || $chunk === '') {
Fiber::suspend($socket); // yield while waiting for more data
} else {
$response .= $chunk;
}
}
fclose($socket);
return $response;
}
}
// Scheduler that watches sockets and resumes fibers when ready
class IoScheduler extends CooperativeScheduler
{
private array $socketToFiber = [];
public function run(): void
{
// Extended scheduler that checks sockets between fiber runs
// Full implementation would use stream_select() for efficiency
parent::run();
}
}
Fibers in production – the practical summary
<?php
// In practice: use ReactPHP or Amp, not raw Fibers
// They provide the scheduler, I/O integration, and Promise/async patterns
// ReactPHP with Fibers (via react/async package)
// - This is the practical use case for Fibers in PHP today
require 'vendor/autoload.php';
use function React\Async\async;
use function React\Async\await;
use function React\Async\parallel;
// Fetch 10 API endpoints in parallel using Fibers under the hood
async(function() {
$urls = array_map(
fn($i) => "https://jsonplaceholder.typicode.com/posts/{$i}",
range(1, 10)
);
$client = new \React\Http\Browser();
// All 10 requests fire concurrently
$tasks = array_map(fn($url) => async(function() use ($client, $url) {
$response = await($client->get($url));
return json_decode($response->getBody(), true);
}), $urls);
$results = await(parallel($tasks));
echo count($results) . " posts loaded in parallel\n";
})();
\React\EventLoop\Loop::run();
Summary
Fibers are PHP’s cooperative concurrency primitive. A Fiber runs until it calls Fiber::suspend(), at which point control returns to whoever called start() or resume(). A scheduler sits on top and decides which Fiber to resume next. ReactPHP, Amp, and Laravel Octane’s async support all use Fibers under the hood – understanding the mechanism helps when debugging async code or building custom concurrency constructs for import pipelines and API fan-out operations.
