Fibers: Cooperative Concurrency, Suspension, and Resumption

Understand what a PHP Fiber is, how Fiber::suspend() and Fiber::resume() hand control back and forth cooperatively, how Fibers differ fundamentally from OS threads, and why Fibers are a low-level primitive that most application code uses only indirectly through async libraries and event loops built on top of them.

Fibers, added in PHP 8.1, are PHP's built-in primitive for pausing and resuming a chunk of code's execution at arbitrary points — not just at function boundaries, the way generators pause only at yield. Before Fibers, PHP's async ecosystem (libraries like ReactPHP and Amp) had to simulate cooperative multitasking using generators and callback-based tricks, which was workable but awkward and leaked implementation details into application code. Fibers give the language itself a clean, first-class way to suspend a call stack and resume it later — but they are explicitly a low-level building block: most day-to-day PHP application code will never call Fiber directly, and instead uses higher-level async libraries built on top of it.

What a Fiber is: a pausable call stack

A Fiber wraps a callable and lets that callable's execution be suspended and resumed from anywhere inside it — including deep inside nested function calls — not just at the top level the way a generator's yield must appear directly in the generator function's own body. This is the key structural difference from generators: a generator can only pause itself, at a yield written directly in its own code, while a Fiber can pause even when the point of suspension is several function calls deep inside the callable it's running, because a Fiber preserves an entire separate call stack, not just one function's local state.

Creating new Fiber($callable) does not start running the callable — like a generator, execution begins only when you call $fiber->start(...$args). Inside the callable (at any depth), calling the static method Fiber::suspend($value) pauses the Fiber's entire call stack right at that point and hands $value back to whoever is controlling the Fiber. Later, calling $fiber->resume($value) on that Fiber object resumes execution exactly where it was suspended, and the $value passed to resume() becomes the return value of the Fiber::suspend() expression that paused it — a two-way handoff of values in both directions.

• new Fiber($callable) — creates the Fiber but does not run it yet. • $fiber->start(...$args) — begins execution; runs until the first Fiber::suspend() or the callable returns. • Fiber::suspend($value) — called from anywhere inside the running Fiber; pauses it and returns $value to the controller. • $fiber->resume($value) — resumes the paused Fiber; $value becomes the result of the Fiber::suspend() call that paused it.

Cooperative concurrency: nothing happens unless you make it happen

Fibers implement cooperative concurrency, meaning a Fiber only ever pauses when its own code explicitly calls Fiber::suspend() — the PHP engine never forcibly interrupts a running Fiber the way an OS scheduler can preempt a thread mid-instruction. This makes Fiber-based code fundamentally easier to reason about than threaded code: within a single Fiber's execution between one suspension point and the next, you have exclusive, uninterrupted access to any shared state, because nothing else can run concurrently with it — 'concurrently' here meaning interleaved on the single underlying OS thread, not literally simultaneous.

This is why Fiber-based (and generator-based) concurrency in PHP does not need mutexes, locks, or atomic operations the way real multi-threaded code does: there is no possibility of two pieces of PHP code running at the exact same instant and corrupting shared state, because the PHP process itself remains single-threaded — Fibers only reorder when different logical tasks get turns to run, they do not run them in parallel.

• A Fiber pauses only at an explicit Fiber::suspend() call — never involuntarily. • Between suspension points, a Fiber's code runs with no interleaving from any other Fiber. • No locks/mutexes are needed for shared state, because true simultaneous execution never happens. • This is concurrency (interleaved progress on multiple tasks), not parallelism (simultaneous execution).

How Fibers differ from OS threads

An OS thread is scheduled preemptively by the operating system, can genuinely run in parallel with other threads on a multi-core machine, and requires synchronization primitives (mutexes, semaphores, atomics) to safely share mutable state, because the OS can switch between threads at essentially any machine instruction. A Fiber runs entirely within a single OS thread, never executes literally in parallel with any other code in that same PHP process, and switches only at points the running code itself chooses by calling Fiber::suspend().

Threads also each carry meaningful OS-level overhead (their own stack allocation, kernel scheduling bookkeeping) and PHP's own threading story (via extensions like pthreads/parallel) additionally means each thread typically gets its own isolated copy of the PHP interpreter's state, since PHP's core data structures were not designed to be safely shared across threads. Fibers, by contrast, are comparatively lightweight, share the same PHP process and memory space naturally (since only one Fiber ever actually executes at a time), and are specifically designed for exactly the pause/resume pattern that async I/O needs — waiting on a network response, a database query, or a timer without blocking everything else that same PHP process could otherwise be doing while it waits.

• OS thread: preemptive scheduling, true parallel execution possible, needs locks for shared state. • Fiber: cooperative scheduling, never truly parallel within one process, no locks needed for shared state. • OS threads are heavier (kernel-managed stacks, scheduling overhead); Fibers are comparatively lightweight, in-process. • Fibers solve 'wait without blocking'; threads solve 'compute in true parallel' — different problems.

Async libraries and event loops: built on Fibers, not part of the language

A raw Fiber, by itself, does not make I/O non-blocking — calling a blocking function like a synchronous database query or file_get_contents() on a slow URL inside a Fiber still blocks that Fiber (and, since PHP is single-threaded, blocks the whole process) exactly as it would outside one. What actually makes async I/O possible is an event loop: a scheduler, provided by a library — not the PHP language itself — that tracks which Fibers are waiting on which I/O operations (sockets, timers, streams), polls the operating system for which of those are ready, and calls resume() on the corresponding Fiber only once its particular operation has actually completed.

This is precisely the classification to keep straight: Fiber is a language-level primitive PHP ships natively, but 'async PHP' as a programming style — non-blocking HTTP clients, concurrent database queries, WebSocket servers — is delivered by ecosystem libraries (ReactPHP, Amp, Swoole, and similar) that build an event loop and a friendly async/await-style API on top of Fibers. Most application developers write code against those libraries' higher-level APIs (which often look like ordinary sequential code, with the Fiber suspension happening invisibly underneath an awaitable promise-like object) and never call Fiber::suspend()/resume() directly — much the way most JavaScript developers use async/await without thinking about the underlying event loop's microtask queue.

For that reason, Fibers belong squarely in the 'advanced, low-level primitive' category: understanding what they are and why they exist is valuable for reading library internals, debugging surprising async behavior, or building infrastructure-level tooling — but writing everyday PHP application code (a typical web request handler, a CLI script, a queue worker) essentially never requires reaching for Fiber directly.

Fiber suspend/resume mechanics

start() and resume() both return whatever value the NEXT Fiber::suspend() call passes, or the fiber's own return value if it finishes instead of suspending again. Fiber::suspend() must be called from code running INSIDE a Fiber — calling it outside one throws FiberError. getReturn() is valid only once isTerminated() is true, mirroring Generator's getReturn() rules.

$fiber = new Fiber(function (int $start): int {
    echo "Fiber started with {$start}" . PHP_EOL;

    $receivedFromResume = Fiber::suspend($start * 2); // pause here, hand back $start*2

    echo "Fiber resumed with {$receivedFromResume}" . PHP_EOL;

    return $receivedFromResume + 100; // becomes $fiber->getReturn()
});

$valueFromSuspend = $fiber->start(21);      // runs until Fiber::suspend(), returns its argument
echo "Controller got: {$valueFromSuspend}" . PHP_EOL;

$fiber->resume(999);                        // resumes the fiber; 999 becomes $receivedFromResume

if ($fiber->isTerminated()) {
    echo 'Final return: ' . $fiber->getReturn() . PHP_EOL;
}

Code Examples

Each Fiber::suspend() call both sends a value out to the controller AND becomes the expression that later receives whatever value the corresponding resume() call sends back in — this is why the Fiber can carry on a two-way, multi-step conversation with its controller across several suspend points, all while preserving its own local variables ($name) between pauses. Expected result: What is your name? Hello, Ada! How old are you? Ada is 37 years old.

$name = Fiber::suspend('What is your name?'); ... $fiber->resume('Ada');

This deliberately shows the misconception to avoid: simply running code inside a Fiber does not grant it non-blocking I/O. usleep() still blocks the entire process for half a second whether or not it's inside a Fiber. Real non-blocking behavior requires an event loop that, instead of calling a blocking function directly, registers interest in a socket/stream becoming ready and calls Fiber::suspend() while waiting, resuming only when the OS reports the I/O is actually ready — that orchestration is what libraries like ReactPHP/Amp/Swoole provide on top of Fibers. Expected result: data from https://example.test after 0.5s

Common mistake: Assuming 'this is inside a Fiber' is sufficient for concurrency — a Fiber only changes WHEN code is allowed to pause; an event loop built around it is what actually makes waiting for I/O not block everything else.

$fiber = new Fiber(function (): void {
    $name = Fiber::suspend('What is your name?');
    $age = Fiber::suspend("Hello, {$name}! How old are you?");
    echo "{$name} is {$age} years old." . PHP_EOL;
});

$question1 = $fiber->start();
echo $question1 . PHP_EOL;

$question2 = $fiber->resume('Ada');
echo $question2 . PHP_EOL;

$fiber->resume(37);
function fakeSlowFetch(string $url): string
{
    // stand-in for a real blocking call, e.g. file_get_contents($url)
    usleep(500_000); // 0.5s — this BLOCKS the whole process, Fiber or not
    return "data from {$url}";
}

$fiber = new Fiber(function (string $url): string {
    // Calling a blocking function inside a Fiber does NOT make it non-blocking —
    // the fiber (and the whole single-threaded process) still stalls here.
    return fakeSlowFetch($url);
});

$start = microtime(true);
$fiber->start('https://example.test');
echo $fiber->getReturn() . ' after ' . round(microtime(true) - $start, 1) . 's' . PHP_EOL;

Fiber suspend/resume vs an OS thread's preemptive switching

A Fiber only yields control at a point its own code chooses; an OS thread can be interrupted by the scheduler at essentially any instruction — this is why Fiber-based code needs no locks, but must still avoid ever blocking inside a suspend/resume-managed section.

Fiber (cooperative, single OS thread):
  [Fiber A runs] --Fiber::suspend()--> [controller/event loop runs] --resume()--> [Fiber A continues]
  (switch happens ONLY where Fiber A's own code calls suspend())

OS threads (preemptive, possibly multi-core):
  [Thread 1 running] <-- OS scheduler can interrupt at ANY instruction --> [Thread 2 running]
  (switch can happen at any point, chosen by the OS, not the code)

Important Rules

Fiber::suspend() only pauses the current point of execution — it does not make any blocking call inside it non-blocking on its own.

A Fiber can only be resumed by the code that controls it (holds the Fiber object); it never resumes itself.

Fibers never run truly in parallel within one PHP process — only one Fiber's code executes at any instant.

Prefer an established async library (ReactPHP, Amp, Swoole) over hand-rolling an event loop with raw Fibers for real application code.

Treat Fiber as infrastructure-level: most application code should depend on a library's async API, not call Fiber::suspend()/resume() directly.

Where this shows up

Fibers are the foundation modern PHP async frameworks (ReactPHP's Fiber-based promise resolution, Amp v3, and Laravel/Symfony's growing async tooling) use internally to let application code write what looks like ordinary sequential logic — 'fetch this, then that' — while the library transparently suspends and resumes Fibers behind an awaitable/promise API as real I/O completes; application developers consume that async/await-flavored surface and essentially never write Fiber::suspend()/resume() by hand, exactly the way most web developers use async/await in JavaScript without touching its event loop's internals directly.

$fiber = new Fiber(fn() => Fiber::suspend('x')); $fiber->resume(); // FiberError: cannot resume a fiber that has not started

$fiber = new Fiber(fn() => Fiber::suspend('x')); $fiber->start(); // must start() before the first resume()

A newly constructed Fiber has not begun executing at all — you must call start() first (which runs the callable until its first suspend or return); calling resume() before any start() is a logic error and throws FiberError, because there is nothing yet to resume.

function slowTask(): string { usleep(1_000_000); return 'done'; } $fiber = new Fiber(fn() => slowTask()); $fiber->start(); // 'using a Fiber' but still blocks the whole process for 1 full second

// Real non-blocking behavior requires an event-loop-integrated I/O call (e.g. an async library's fetch/query helper) that itself calls Fiber::suspend() and is resumed by the loop once the OS reports the I/O is ready.

Simply wrapping a blocking call in a Fiber does not make it non-blocking — the Fiber (and the single PHP thread it runs on) still stalls for the full duration of usleep()/any other blocking call. Genuine non-blocking I/O requires cooperation from an event loop that suspends while waiting on the OS, not just the presence of a Fiber.

What triggers a Fiber to pause its execution? A. The PHP engine automatically pauses it after a fixed time slice, like an OS thread B. An explicit call to Fiber::suspend() from code running inside the fiber C. Any function call inside the fiber automatically pauses it D. Fibers cannot be paused once started

B. An explicit call to Fiber::suspend() from code running inside the fiber — Fibers are cooperative: they only pause at an explicit Fiber::suspend() call written into the code running inside them — the engine never preempts a Fiber the way an OS scheduler preempts a thread.

What is the key structural difference between a Fiber and a generator? A. Generators can run in true parallel; Fibers cannot B. A generator can only pause at a yield written directly in its own function body, while a Fiber can pause from any depth of nested function calls inside it C. Fibers cannot return a value; generators can D. There is no real difference — Fiber is just a renamed Generator

B. A generator can only pause at a yield written directly in its own function body, while a Fiber can pause from any depth of nested function calls inside it — A generator's yield must appear directly in the generator function's own code, pausing only that one function. A Fiber preserves an entire separate call stack, so Fiber::suspend() can be called from deep inside nested function calls and still correctly pause and later resume the whole stack.

Why don't Fibers require mutexes or locks to safely share mutable state, unlike OS threads? A. Fibers automatically copy all shared state before running B. Because only one Fiber's code ever actually executes at a given instant in a single PHP process — there is no true simultaneous execution to corrupt shared state C. PHP arrays are inherently thread-safe D. Locks are still required; this is a common misconception

B. Because only one Fiber's code ever actually executes at a given instant in a single PHP process — there is no true simultaneous execution to corrupt shared state — Fiber-based concurrency is cooperative and confined to a single OS thread, so exactly one Fiber's code runs at any given instant — there is never true simultaneous execution within that process, which is precisely why no locking is needed for shared state between suspension points.

Does wrapping a blocking function call (like a synchronous slow database query) inside a Fiber make it non-blocking? A. Yes, any code inside a Fiber automatically becomes non-blocking B. No — a Fiber only changes where code can be suspended; making I/O actually non-blocking requires an event loop (from an async library) that suspends while waiting and resumes once the OS reports the I/O is ready C. Yes, but only for network calls, not database calls D. No, Fibers have nothing to do with I/O at all

B. No — a Fiber only changes where code can be suspended; making I/O actually non-blocking requires an event loop (from an async library) that suspends while waiting and resumes once the OS reports the I/O is ready — A raw Fiber by itself does not make blocking calls non-blocking — a blocking call inside a Fiber still blocks the whole single-threaded process. True non-blocking async behavior requires an event loop, provided by an ecosystem library, that suspends the Fiber while an I/O operation is pending and resumes it only once that operation is actually ready.

Which statement best reflects how Fibers should be classified relative to typical application code? A. Every PHP web request handler should call Fiber::suspend()/resume() directly for best performance B. Fiber is a low-level, advanced primitive most application code never touches directly — async libraries and event loops are built on top of it and expose a friendlier API instead C. Fibers replace the need for any async library entirely D. Fibers are deprecated in favor of threads

B. Fiber is a low-level, advanced primitive most application code never touches directly — async libraries and event loops are built on top of it and expose a friendlier API instead — Fibers are foundational infrastructure: async libraries (ReactPHP, Amp, Swoole, etc.) build event loops and promise/async-await-style APIs on top of Fibers, and most application developers consume those higher-level APIs without ever calling Fiber::suspend()/resume() themselves.

Write a Fiber whose callable takes no arguments, suspends three times yielding the strings 'step1', 'step2', and 'step3' in turn, and returns the string 'complete' after the third suspend is resumed. Drive it from outside with start() and three resume() calls, printing each value that comes back from start()/resume(), and finally print $fiber->getReturn() once isTerminated() is true.

$fiber = new Fiber(function (): string { Fiber::suspend('step1'); Fiber::suspend('step2'); Fiber::suspend('step3'); return 'complete'; }); echo $fiber->start() . PHP_EOL; // step1 echo $fiber->resume() . PHP_EOL; // step2 echo $fiber->resume() . PHP_EOL; // step3 $fiber->resume(); // runs to the return; isTerminated() becomes true if ($fiber->isTerminated()) { echo $fiber->getReturn() . PHP_EOL; // complete }

Each Fiber::suspend('stepN') call pauses and returns 'stepN' to whichever start()/resume() call triggered that run; the value the fiber's function finally returns (not yields) becomes getReturn() only after the third resume() lets it fall off the end.