Fibers and the Async Library Ecosystem

Clearly distinguish PHP's native Fiber primitive (suspension and resumption only) from the async HTTP clients, timers, sockets, and schedulers that third-party packages like ReactPHP, Amp, and Revolt build on top of it.

By now you've met PHP's Fiber class in depth across two earlier lessons — one on Fiber basics and one on integrating Fibers with an event loop. This lesson does not repeat that material; instead it zooms out to answer a question those lessons set up but didn't fully resolve: when you use Amp\... or use React\... and get async HTTP requests, timers, and socket handling that all 'just work' together, how much of that is the PHP language, and how much is the library? The answer matters because it tells you what you can rely on from any PHP 8.1+ installation versus what you're pulling in from Composer.

Recap: what Fiber actually is

The two earlier Fibers lessons cover this in full: Fiber is a language-level primitive, added in PHP 8.1, that lets a block of code pause its own execution with Fiber::suspend() and be resumed later with $fiber->resume(), preserving its local variables and call stack exactly as they were. That is the entire feature. A Fiber object does not know how to read a socket, does not know how to wait for a timer, and does not have any concept of "come back to me when this HTTP response arrives" — it only knows how to pause and how to be told to continue.

This is worth stating plainly because it's the single most common point of confusion for developers coming from languages where async/await is deeply integrated with I/O (like JavaScript or Python's asyncio): PHP's Fiber gives you the *suspension mechanism* async code needs, but nothing that decides *when* to resume a fiber. Something else has to do that.

What third-party packages add on top

This is where ReactPHP, Amp, and the shared Revolt event loop (which Amp is built on, and which other libraries can plug into) come in. None of what they provide is a PHP language feature — it is all ordinary PHP code, shipped as Composer packages, that uses Fiber (or, in ReactPHP's older APIs, plain callbacks) as a building block to implement real infrastructure:

• An actual event loop — a running while loop that calls into the OS's stream_select(), epoll, or similar, to find out which sockets/timers/streams are ready — packages provide this; the language does not. • Async HTTP clients (Amp's amphp/http-client, ReactPHP's react/http) that open non-blocking sockets, speak the HTTP protocol over them, and resolve a promise/future (or resume a Fiber) once a response arrives. • Timers (Loop::delay() in Amp, $loop->addTimer() in ReactPHP) that let code schedule 'run this later' or 'run this every N seconds' — implemented as data structures the event loop consults on each iteration, not as anything built into PHP itself. • Socket and stream handling wrappers that turn PHP's low-level (and originally blocking-by-default) stream functions into non-blocking, event-driven ones. • A scheduler that decides, each time around the event loop, which suspended fiber gets resumed next, based on which of its awaited operations became ready.

Why the distinction matters in practice

If you write $fiber = new Fiber(function () { ... Fiber::suspend(); ... }); $fiber->start(); with nothing else, you have working suspension and resumption — but you still have to call $fiber->resume() yourself, by hand, at exactly the right time. There is no timer, no socket watcher, nothing making that happen automatically. This is precisely what "Fibers and Event-Loop Integration" demonstrates: it shows the wiring code (typically supplied by a library, or written by hand as a teaching exercise) that connects a fiber's suspension to an actual I/O readiness check.

Practically, this means: Fiber ships with every PHP 8.1+ installation and needs no composer require — but async HTTP requests, non-blocking sockets, and timers always come from a package (amphp/*, react/*, or something built on Revolt). Knowing this helps you read documentation and error messages correctly: a 'fiber has already been resumed' error is about the language primitive; a 'no event loop is running' error is about the library's infrastructure sitting on top of it.

Layering: language feature vs. package infrastructure

Everything below the top line is ordinary PHP code you install; nothing below the top line is a language feature.

PHP language (built in, PHP 8.1+)
  Fiber           -- suspend()/resume() only, no I/O awareness

Third-party packages (installed via Composer)
  Revolt / ReactPHP event loop  -- the running loop that polls sockets & timers
  Amp / ReactPHP promises-futures -- the pending/fulfilled/rejected wrapper
  amphp/http-client, react/http    -- async HTTP built using the loop
  Loop::delay(), $loop->addTimer() -- timers built using the loop
  async socket wrappers            -- non-blocking streams built using the loop

Code Examples

This is the entire native Fiber feature in action: pause, hand control back to the caller, and require an explicit resume() call. There is no I/O anywhere in this example on purpose — it demonstrates that Fiber itself has nothing to say about *when* resume() should happen. In a real async library, an event loop's readiness check ("this socket now has data") is what decides to call resume() instead of a human reading the next line of the script. Expected result: Fiber: about to suspend, waiting for a value... Main: fiber suspended with 'paused' Fiber: resumed with value = hello from main Main: fiber returned 'fiber finished'

$result = $fiber->resume('hello from main');

Compare this to the bare-Fiber example above: the suspend/resume calls still happen, but they are now hidden inside amphp/http-client and Amp's event loop — the package is doing exactly the manual work the previous example made you do by hand, triggered by real socket readiness instead of the next line of a script. This snippet is illustrative of the pattern, not a fully self-contained runnable program — it requires the amphp/http-client package and its dependencies to actually execute.

$client->request(new Amp\Http\Client\Request('https://example.com'));

<?php
declare(strict_types=1);

$fiber = new Fiber(function (): string {
    echo "Fiber: about to suspend, waiting for a value...\n";
    $value = Fiber::suspend('paused');
    echo "Fiber: resumed with value = {$value}\n";
    return "fiber finished";
});

$startValue = $fiber->start(); // runs until the first suspend()
echo "Main: fiber suspended with '{$startValue}'\n";

// Nothing automatic happens here. No event loop, no timer, no socket watcher
// is going to call resume() for us -- WE must call it, by hand, right now.
$result = $fiber->resume('hello from main');
echo "Main: fiber returned '{$result}'\n";
<?php
// Illustrative only -- see "Fibers and Event-Loop Integration" for the full,
// runnable version of this pattern.
use Amp\Http\Client\HttpClient;
use function Amp\async;
use function Amp\delay;

$response = async(function () {
    $client = new HttpClient();
    // Amp suspends the current fiber internally here and resumes it
    // once its event loop sees the HTTP response arrive -- YOU never
    // call Fiber::suspend() or ->resume() yourself.
    $response = $client->request(new Amp\Http\Client\Request('https://example.com'));
    return $response->getBody()->buffer();
})->await();

echo $response;

Who calls resume()?

Bare Fiber (language only):
  your code --calls--> $fiber->resume()   [you decide when, by hand]

Fiber + async library (Amp / ReactPHP / Revolt):
  event loop --detects socket ready--> library internals --calls--> $fiber->resume()
  [the library decides when, triggered by real I/O]

Important Rules

Fiber (the class) ships with PHP 8.1+ and requires no package; async HTTP, timers, and non-blocking sockets always come from a third-party package.

A fiber never resumes itself — something external always calls resume(); that 'something' is either your own code (bare Fiber) or a library's event loop (in a real async stack).

When debugging, separate 'is this a Fiber-level problem' (suspend/resume misuse) from 'is this an event-loop-level problem' (nothing scheduled a resume) — they have different fixes.

Reading a stack trace or an install list correctly

When you see composer.json requiring amphp/http-client or react/event-loop, you now know exactly why: Fiber alone in PHP core could never provide those — the packages exist specifically to supply the event loop, the I/O primitives, and the scheduling that the language deliberately leaves out. This is also why different frameworks can mix and match: Revolt exists as a shared, neutral event loop specifically so multiple packages (Amp's HTTP client, various socket libraries) can cooperate on one loop instead of each bundling an incompatible one.

Assuming that using new Fiber(...) in your code automatically gives you non-blocking HTTP requests or sockets.

Recognize that Fiber only provides suspension/resumption; actual async I/O (HTTP clients, timers, sockets) requires an event-loop package such as ReactPHP, Amp, or something built on Revolt.

A bare Fiber wrapped around a normal blocking call like file_get_contents() still blocks the whole process during that call — nothing about Fiber makes an I/O call non-blocking by itself. Only a library that replaces the actual I/O with non-blocking, event-loop-driven equivalents makes it async.

Which of the following does PHP's native Fiber class provide by itself, with no third-party package? A. Non-blocking HTTP requests B. An event loop that polls sockets for readiness C. The ability to suspend and later resume a block of code's execution, preserving its local state D. Built-in timers for scheduling delayed callbacks

C. The ability to suspend and later resume a block of code's execution, preserving its local state — Fiber is purely a suspension/resumption primitive. HTTP clients, event loops, and timers are all supplied by third-party packages like ReactPHP or Amp built on top of it.

In an async library like Amp, what ultimately decides *when* a suspended fiber's resume() is called? A. PHP's garbage collector, once the fiber's memory is scanned B. The library's event loop, once it detects the awaited I/O (socket, timer, etc.) is ready C. A fixed, hardcoded delay built into the Fiber class itself D. The next line of the script, automatically, with no scheduling involved

B. The library's event loop, once it detects the awaited I/O (socket, timer, etc.) is ready — The event loop is the piece — always supplied by a package, never by the language — that watches pending I/O and calls resume() on the appropriate fiber once its data is ready.

Without installing any package, write a tiny 'micro event loop' that manages two Fibers, each of which suspends with a number of ticks to wait, and resumes each fiber only once that many of your own manual loop iterations have passed. Hint: Store each fiber alongside a remaining-ticks counter; each loop iteration, decrement every counter and resume() any fiber whose counter hits zero.

Try it yourself — there's no single right answer here; compare your result against this lesson's examples.