PHP Fibers — Internals & Scheduler Patterns
debt(d7/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). PHPStan is listed but cannot reliably detect semantic misuse like blocking I/O inside fibers or improper pooling. The detection_hints explicitly state automated=no. Blocking calls (sleep, PDO) inside fibers look syntactically valid and only manifest as performance issues at runtime under load.
Closest to 'touches multiple files / significant refactor in one component' (e5). The quick_fix says 'use non-blocking async equivalents from Revolt or ReactPHP' — this requires replacing PDO with async database drivers, file_get_contents with async HTTP clients, sleep() with async timers. This is not a one-line fix; it's a component-level refactor to swap blocking for non-blocking equivalents throughout the fiber-based code.
Closest to 'persistent productivity tax' (b5). Fibers apply only to cli/queue-worker contexts per applies_to, limiting reach. However, once adopted for async patterns, every I/O call must use async-aware libraries, every new developer must understand suspension semantics, and debugging async stack traces requires fiber-aware tooling. This is a persistent tax on the async portions of the codebase.
Closest to 'serious trap' (t7). The misconception field explicitly states developers wrongly believe 'PHP Fibers enable true parallel execution' when they're actually cooperative single-threaded concurrency. This contradicts how concurrency primitives work in other languages (Go goroutines with preemption, threads in Java/Python). Additionally, common_mistakes reveal multiple non-obvious behaviors: suspend() outside fiber throws, exceptions can be thrown into fibers, each fiber consumes 2MB stack.
Also Known As
TL;DR
Explanation
A PHP Fiber is a stackful coroutine — it gets its own call stack (unlike generators which resume from a single yield point). Internally, the Zend Engine allocates a separate C stack for each fiber (configurable via `fiber.stack_size` in php.ini, default 2MB) and saves/restores CPU registers and the stack pointer on suspend/resume. `Fiber::suspend($value)` saves the current stack frame, returns control to the caller, and passes `$value` out. `$fiber->resume($value)` restores the stack frame and passes `$value` back in as the return value of `Fiber::suspend()`. Unlike threads, fibers are cooperative — only one fiber runs at a time, switching only at explicit `Fiber::suspend()` calls, so no mutexes are needed for shared state. A scheduler is a loop that tracks pending fibers and resumes them when their awaited condition is met (I/O ready, timer elapsed). Libraries like ReactPHP and Revolt PHP use fibers to implement `async/await`-style concurrency on top of an event loop.
Diagram
sequenceDiagram
participant S as Scheduler
participant FA as Fiber A
participant FB as Fiber B
S->>FA: start()
FA->>FA: step 1
FA->>S: Fiber::suspend()
S->>FB: start()
FB->>FB: step 1
FB->>S: Fiber::suspend()
S->>FA: resume()
FA->>FA: step 2
FA->>S: terminated
S->>FB: resume()
FB->>FB: step 2
FB->>S: terminated
Common Misconception
Why It Matters
Common Mistakes
- Not handling exceptions thrown into a fiber — `$fiber->throw(new Exception)` resumes the fiber with an exception at the suspend point; uncaught, it propagates to the caller.
- Blocking inside a fiber (sleep(), PDO query without async driver) — blocks the entire PHP process, defeating the purpose of cooperative concurrency.
- Creating thousands of fibers simultaneously — each fiber allocates a full stack (default 2MB); 1000 fibers = 2GB RAM. Pool and reuse fibers for high-concurrency workloads.
- Calling `Fiber::suspend()` outside a fiber — throws a `FiberError`; always check `Fiber::getCurrent() !== null`.
Code Examples
// Blocking inside a fiber — defeats cooperative concurrency:
$fiber = new Fiber(function(): void {
sleep(2); // blocks entire process — no other fiber runs
$result = file_get_contents('https://example.com'); // blocking I/O
Fiber::suspend($result);
});
$fiber->start();
// Simple cooperative scheduler using Fibers + non-blocking I/O:
class Scheduler {
private array $fibers = [];
public function add(Fiber $fiber): void {
$this->fibers[] = $fiber;
}
public function run(): void {
while ($this->fibers) {
foreach ($this->fibers as $key => $fiber) {
if (!$fiber->isStarted()) $fiber->start();
elseif ($fiber->isSuspended()) $fiber->resume();
if ($fiber->isTerminated()) {
unset($this->fibers[$key]);
}
}
}
}
}
$scheduler = new Scheduler();
$scheduler->add(new Fiber(function(): void {
echo "Task A: step 1\n";
Fiber::suspend(); // yield control
echo "Task A: step 2\n";
}));
$scheduler->add(new Fiber(function(): void {
echo "Task B: step 1\n";
Fiber::suspend();
echo "Task B: step 2\n";
}));
$scheduler->run();
// Output: Task A: step 1 / Task B: step 1 / Task A: step 2 / Task B: step 2