PHP Runtime And Server Environment
Fibers, Coroutines, And Non-Blocking I/O
This lesson is about the runtime consequences. The advanced language lesson explains fiber mechanics in detail. Here the question is more practical: when a PHP project uses fibers, coroutines, or async-style APIs, what has to be true around the code for non-blocking I/O to be real?
The short answer is that the whole path must cooperate. A fiber can pause your PHP call stack. An event loop can watch resources and timers. A non-blocking client can start I/O without waiting. A scheduler can resume the right fiber when the operation is ready. If one layer secretly calls a blocking function, the process still waits.
Official reference:
A Small Fiber Example
This script uses only PHP's built-in Fiber class. It starts a fiber, suspends it with a value, resumes it with another value, and then reads the final return value.
<?php
declare(strict_types=1);
$fiber = new Fiber(function (): string {
echo 'Fiber started' . PHP_EOL;
$value = Fiber::suspend('waiting');
echo 'Fiber resumed with ' . $value . PHP_EOL;
return 'done';
});
echo $fiber->start() . PHP_EOL;
$fiber->resume('data');
echo $fiber->getReturn() . PHP_EOL;
// Prints:
// Fiber started
// waiting
// Fiber resumed with data
// done
The important detail is control flow. start() runs until the fiber suspends. Fiber::suspend() returns a value to the caller. resume() sends a value back into the suspended fiber. After the callback returns, getReturn() reads its return value.
Nothing in this example performs I/O. The fiber is useful for understanding suspension, not for proving that code is non-blocking.
Coroutines As A Runtime Idea
A coroutine is a routine that can pause and continue later. Different PHP ecosystems use the word differently. Swoole and OpenSwoole have coroutine features through extensions and runtime hooks. Amp uses fibers in modern PHP to present async APIs that can look sequential. ReactPHP historically exposes callbacks, promises, streams, and loop primitives more directly, while its async utilities can also help structure asynchronous flows.
Do not get stuck on the label. Ask what actually happens when the code waits. Does the current operation give control back to a scheduler? Can other sockets and timers run while it waits? How are timeout, cancellation, and errors represented? Which library resumes the work?
A coroutine-style API can be easier to read than callback-heavy code, but readability is not the same as safety. A neat sequential-looking function can still block the process if it calls a synchronous client inside the coroutine.
Blocking I/O Still Blocks
Blocking I/O waits until the operation finishes:
<?php
declare(strict_types=1);
$contents = file_get_contents(__FILE__);
echo is_string($contents) ? 'read complete' : 'read failed';
echo PHP_EOL;
// Prints:
// read complete
That is acceptable in many scripts. In an event loop, blocking reads, sleeps, HTTP calls, DNS lookups, database calls, and process waits can stop other tasks from progressing. A fiber does not automatically move that operation to another thread or ask the operating system to notify PHP later.
Wrapping blocking code in a fiber proves the point:
<?php
declare(strict_types=1);
$fiber = new Fiber(function (): void {
sleep(1);
echo 'Blocking work finished' . PHP_EOL;
});
$fiber->start();
echo 'After fiber' . PHP_EOL;
// Prints:
// Blocking work finished
// After fiber
The process still waits during sleep(1). The code after start() does not run until the fiber callback reaches the end, throws, or suspends. Because this fiber never suspends before sleeping, it holds control like ordinary synchronous PHP.
What Async Libraries Add
Async libraries provide the missing runtime pieces around fibers or callbacks:
- an event loop that watches sockets, timers, signals, and scheduled callbacks
- non-blocking stream, socket, DNS, HTTP, process, and timer integrations
- a representation for future results, such as promises, futures, or awaitables
- timeout and cancellation mechanisms
- error propagation from async operations back to the caller
- concurrency limits so one process does not start unlimited work
- cleanup rules for streams, timers, listeners, and pending operations
A fiber-aware library can let application code write something that looks like fetchProfile($id) while the library actually starts non-blocking I/O, suspends the current fiber, lets the loop continue, and resumes the fiber when the response is available.
That abstraction is valuable because it keeps business code readable. It is also easy to misuse. If fetchProfile() eventually calls a blocking HTTP client, the sequential style hides the problem instead of solving it.
Fibers, Polling, And Event Loops
A useful async mental model has three layers. Fibers pause and resume PHP call stacks. Polling or readiness APIs ask the operating system which streams or sockets can make progress. An event loop connects those two ideas by waiting for readiness, timers, signals, or scheduled callbacks, then resuming the appropriate work.
This separation matters because the layers are often discussed as if they are the same feature. A fiber is not an event loop. A polling API is not a complete application runtime. An event loop is not automatically safe just because it can resume fibers. Useful async PHP needs all of the layers to cooperate with non-blocking clients, cancellation, limits, and cleanup.
At the operating-system level, scalable network programs usually do not ask every socket whether it is ready in a tight loop. They register interest in readable or writable resources and wait for the OS to report what changed. On Linux that often means epoll; on BSD and macOS that often means kqueue. Older PHP userland event loops have commonly relied on stream_select() or optional extensions to reach those mechanisms through different backends.
Newer PHP runtime work around polling APIs is important because it points toward a common native readiness primitive for event-loop libraries. The practical lesson for application developers is still conservative: do not hand-roll a scheduler for production. Let maintained libraries such as Amp, ReactPHP, Revolt-based tooling, or a purpose-built runtime own the loop, backend choice, timers, cancellation, and stream integration.
When you see example code that combines Fiber::suspend() with socket readiness, read it as an infrastructure demonstration. It can teach why concurrent I/O finishes near the slowest operation rather than the sum of every wait. It does not automatically include DNS handling, TLS behavior, cancellation tokens, backpressure, error propagation, fairness, observability, or graceful shutdown. Those missing pieces are exactly why async runtimes exist.
For course work and production review, the key question is not "does this code mention fibers?" The key question is "who owns the event loop, how does it learn that I/O is ready, and what happens when an operation is slow, cancelled, failed, or too numerous?" That question keeps the language feature separate from the runtime engineering.
The Boundary Audit
When reviewing fiber or coroutine code, trace the wait boundaries. Every place that can wait needs a runtime answer.
For each boundary, ask:
- Is this operation CPU-bound or I/O-bound?
- If it is I/O-bound, does the client library explicitly support the selected async runtime?
- Is there a timeout budget?
- Can the operation be cancelled when the caller disconnects or the job is stopped?
- Where does the error go?
- Is concurrency bounded?
- What cleanup happens if the operation is abandoned?
This audit is more useful than asking whether the code contains Fiber. A project can use fibers correctly through Amp or another runtime. A project can also use raw fibers and still block everywhere.
Databases, Files, And External Services
Database access is a common trap. Many PHP database clients are synchronous. If a fiber-aware service calls a blocking PDO query inside an event loop, the loop waits. Some runtimes provide database integrations, connection pools, or guidance for moving database work out of the loop. Use what the selected runtime supports; do not assume a familiar client becomes non-blocking automatically.
Filesystem access has similar caveats. Small local reads during startup are usually fine. Repeated large file reads inside a hot callback can block the process and create latency spikes. If the work is heavy, move it to a worker, stream it through supported non-blocking primitives, or keep it outside the event-loop path.
External services need timeout and retry design. A suspended fiber waiting on an upstream call is still consuming memory and possibly holding domain state. Starting ten thousand suspended operations without limits can exhaust the process even if none of them blocks the loop directly.
Cancellation And Cleanup
Suspension makes cancellation important. If a client disconnects while a fiber is waiting for an upstream response, should the upstream request be cancelled? Should the result be ignored? Should the operation continue because it has already created a side effect?
Raw PHP fibers can be resumed by throwing an exception into them. Async runtimes build higher-level cancellation concepts on top of similar control-flow ideas. Application code should use the runtime's documented cancellation API rather than throwing arbitrary exceptions into fibers it does not own.
Cleanup belongs in finally blocks, stream close handlers, timer cancellation code, and runtime-specific disposal hooks. The goal is simple: no abandoned timers, no leaked listeners, no unbounded buffers, no stale per-request state, and no half-recorded operation state after cancellation.
Fibers, Threads, And Parallelism
A fiber is not a thread. It does not run at the same time as another fiber on another CPU core. It is a cooperative execution context inside one PHP process. Only one fiber is running on that thread of execution at a time.
This distinction matters for performance claims. Fibers can improve throughput for I/O-bound work because the process can do other useful work while operations wait. They do not make CPU-heavy PHP loops faster. For CPU-bound work, use separate worker processes, queues, extensions, native libraries, or services designed for parallel execution.
If a benchmark says fibers made an application faster, ask what was being measured. If the bottleneck was waiting on many network responses, the result may be real. If the bottleneck was calculating one large report, fibers alone should not help.
Testing Fiber-Aware Code
Test fiber-aware code at two levels. First, test pure business logic without the async runtime by putting runtime-specific clients behind interfaces. This keeps domain rules easy to verify.
Second, run integration tests through the selected runtime. These tests should cover timeout, cancellation, rejected operations, slow consumers, connection close, concurrency limits, and shutdown. A unit test that calls a function directly may miss the exact failure that happens only when the event loop is running.
Add a test that deliberately uses a slow fake dependency and proves the process still handles another ready operation. That kind of test exposes accidental blocking better than checking for a Fiber object.
When To Use Raw Fibers
Most application developers should not build their own scheduler from raw fibers. Use raw fibers when you are studying the language feature or building infrastructure that genuinely needs to control suspension. In product code, prefer maintained runtime abstractions with documented timeout, cancellation, and I/O behavior.
Normal synchronous PHP is still the right choice for many applications. It is easier to deploy, debug, profile, and reason about. Fibers become valuable when an application has enough I/O concurrency to justify an async runtime and the required libraries actually support that runtime.
What To Check
Before moving on, make sure you can:
- explain that a fiber pauses and resumes a call stack
- explain why a fiber is not a thread and does not create CPU parallelism
- show that blocking code inside a fiber still blocks the process
- describe what an async runtime adds around fibers
- audit database, filesystem, HTTP, DNS, and process calls for blocking behavior
- explain cancellation and cleanup concerns for suspended work
- choose runtime-supported clients rather than ordinary synchronous clients inside an event loop
- decide when synchronous PHP is simpler and safer
After this lesson, you should be able to read PHP code that mentions fibers, coroutines, or non-blocking I/O and separate the language feature from the runtime system. The professional skill is not using Fiber directly; it is proving that every waiting boundary has compatible I/O, bounded concurrency, timeout handling, cancellation, cleanup, and useful diagnostics.
Practice
Task: Demonstrate A Fiber
Create a small PHP script that demonstrates pausing and resuming a fiber.
Requirements
- Start a fiber.
- Suspend it with a value.
- Resume it with another value.
- Print the output in order.
- Add a short note explaining why this does not automatically make blocking I/O non-blocking.
Check your work
The script should run without installing an async framework. The explanation should distinguish fibers from event loops and threads.
Show solution
<?php
declare(strict_types=1);
$fiber = new Fiber(function (): string {
echo 'Inside fiber before suspend' . PHP_EOL;
$received = Fiber::suspend('value from suspend');
echo 'Inside fiber after resume: ' . $received . PHP_EOL;
return 'fiber return value';
});
echo $fiber->start() . PHP_EOL;
echo $fiber->resume('value from resume') . PHP_EOL;
// Prints:
// Inside fiber before suspend
// value from suspend
// Inside fiber after resume: value from resume
// fiber return value
The fiber pauses and resumes, but it does not create a second thread. If the code inside the fiber calls a blocking function, the process still waits. Non-blocking I/O needs an event loop and compatible I/O libraries, not only fiber syntax.
Task: Identify Blocking Boundaries
- validates a JSON payload in memory
- calls a traditional PDO query
- calls an async HTTP client supported by the runtime
- writes a small audit line with
file_put_contents() - waits for a runtime timer before retrying temporary failures
Classify each step as safe, risky, or unsafe inside the event-loop path.
Requirements
Explain which steps can block the process, which depend on runtime support, and which should be moved, replaced, bounded, or kept outside the hot path.
Check your work
The answer should focus on waiting boundaries rather than on whether the surrounding code uses fibers.
Show solution
In-memory JSON validation is usually safe if the payload size is bounded. It is CPU work, but for small messages it should return quickly. Add size limits so a large payload cannot monopolize the process.
A traditional PDO query is unsafe inside the event-loop path because PDO is commonly used synchronously. While the query waits on the database, the loop cannot run other ready work. Use a runtime-supported database client if one exists, move the query to a worker, or keep this service synchronous.
The async HTTP client is safe only because it is explicitly supported by the runtime. It still needs a timeout, cancellation policy, error handling, and concurrency limits. Starting unlimited async HTTP calls can exhaust memory even when the calls are non-blocking.
file_put_contents() is risky in the hot path. A tiny local write may appear harmless, but filesystem latency can spike and block the process. Prefer buffered logging handled by the runtime, a non-blocking stream where supported, or moving audit persistence to a queue or separate worker.
A runtime timer is safe when it is managed by the selected event loop and cancelled when no longer needed. It should be bounded by maximum attempts and should not keep abandoned message state alive forever.
The review conclusion is that fibers do not decide safety. Each waiting boundary must either be quick and bounded, supported by the runtime, or moved out of the event-loop path.
Task: Design A Fiber-Aware Adapter Boundary
A domain service needs customer profile data. It currently calls a synchronous HTTP client directly. The team wants to reuse the domain service from both PHP-FPM requests and an async worker that uses fibers.
Design an adapter boundary for profile lookup.
Requirements
Include:
- an interface the domain service can depend on
- one synchronous implementation option
- one async/runtime implementation option
- how timeouts and errors are represented
- how tests avoid depending on the real async runtime for pure domain rules
Check your work
The design should avoid putting runtime-specific fiber checks throughout the domain service.
Show solution
Define a domain-facing interface such as CustomerProfileLookup with a method that returns a profile result or throws a domain-specific lookup exception. Keep the method name about the business need, not the transport or runtime.
In a PHP-FPM context, the implementation can use the existing synchronous HTTP client with explicit connect and response timeouts. It should translate HTTP timeouts, 404 responses, and malformed provider responses into documented application exceptions or result objects.
In the async worker, provide a runtime-specific implementation that uses the selected non-blocking HTTP client and timeout/cancellation API. That implementation owns the promise, future, awaitable, or fiber behavior. It should not leak raw runtime objects into the domain service unless the whole domain layer is intentionally async-aware.
Pure domain tests can use a fake CustomerProfileLookup that returns known profiles or throws known exceptions. Those tests do not need the event loop. Separate integration tests should run the async implementation through the real runtime and cover timeout, cancellation, rejection, and shutdown behavior.
This boundary keeps runtime code at the edge. The domain service asks for a profile and handles domain outcomes; the adapter decides how waiting, cancellation, and transport errors work in each execution model.