Home PHP PHP Fibers Explained — Async Concurrency, Internals & Production Patterns

PHP Fibers Explained — Async Concurrency, Internals & Production Patterns

In Plain English 🔥
Imagine a chef in a kitchen who can only do one thing at a time. Traditional PHP is that chef — they start boiling pasta, stand there staring at the pot, then plate it, then start the sauce. A Fiber is like giving that chef a magic pause button. They start the pasta, press pause, go chop vegetables, press pause, come back to drain the pasta — all in one kitchen, one chef, zero waiting around. The chef never actually does two things simultaneously, but they stop wasting time standing idle. That's PHP Fibers: one thread, smarter task-switching, no more standing and watching a pot boil.
⚡ Quick Answer
Imagine a chef in a kitchen who can only do one thing at a time. Traditional PHP is that chef — they start boiling pasta, stand there staring at the pot, then plate it, then start the sauce. A Fiber is like giving that chef a magic pause button. They start the pasta, press pause, go chop vegetables, press pause, come back to drain the pasta — all in one kitchen, one chef, zero waiting around. The chef never actually does two things simultaneously, but they stop wasting time standing idle. That's PHP Fibers: one thread, smarter task-switching, no more standing and watching a pot boil.

For most of PHP's history, writing concurrent code meant reaching for extensions like Swoole or ReactPHP, spawning child processes, or just accepting that your script would block on every I/O call like a student copying one word at a time from a textbook. PHP 8.1 changed that fundamentally by shipping Fibers as a first-class language primitive — no PECL, no build flags, just PHP. This is a bigger shift than most developers realise, because it changes what's possible inside the language itself rather than bolting concurrency on from the outside.

The problem Fibers solve is deceptively simple: PHP's traditional request-response model means every blocking call — a database query, an HTTP request to a third-party API, a file read — freezes your entire execution stack until it finishes. You're paying the full wall-clock cost of every wait. Fibers give you the ability to suspend a unit of work mid-execution, hand control back to a scheduler, let something else run, then resume exactly where you left off — with the local variable state fully intact. This is cooperative concurrency, not parallelism, which is a critical distinction we'll dig into.

By the end of this article you'll understand exactly how Fibers work at the C extension level, how to build a minimal event loop that drives multiple Fibers concurrently, what the real performance implications are in production, and the sharp edges that will bite you if you're not paying attention. You'll also understand why Fibers alone aren't magic — they're the primitive that libraries like ReactPHP and Amp use as their foundation, and understanding the primitive makes you dangerous with any library built on top of it.

How PHP Fibers Actually Work — Under the Hood

A Fiber in PHP is a stackful coroutine. That word 'stackful' is doing a lot of work, so let's unpack it.

When you call a regular PHP function, it gets a frame pushed onto the call stack. When it returns, that frame is popped. There's no way to pause halfway through and come back — the frame is gone on return. A Fiber maintains its own separate call stack in memory. When you call Fiber::suspend(), the Fiber's stack is preserved exactly as-is — every local variable, every nested function call depth — and control transfers back to whatever code called $fiber->resume(). When you resume it, the stack is restored and execution continues from the exact suspension point.

Internally, PHP's Fiber implementation (in ext/fiber and Zend/zend_fibers.c) uses platform-specific context-switching via ucontext_t on POSIX systems and fibers via setjmp/longjmp-style switching on Windows. Each Fiber gets a configurable stack (default 8MB on most platforms). This is NOT green threads — there's no OS scheduler involvement and no actual parallelism. One Fiber runs at a time, on the same OS thread, period.

Understanding this cooperative model matters for production: if one Fiber runs a CPU-heavy operation without suspending, all other Fibers starve until it finishes. The responsibility for yielding is entirely yours.

fiber_internals_demo.php · PHP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
<?php

// Requires PHP 8.1+
// This demo reveals the internal lifecycle of a Fiber step by step.

$emailSenderFiber = new Fiber(function (): string {
    // --- FIBER STARTS: runs when $emailSenderFiber->start() is called ---
    echo "[Fiber] Starting: connecting to mail server..." . PHP_EOL;

    // Suspend the Fiber, sending a status value OUT to the caller.
    // Execution pauses HERE. The local state (including this closure) is frozen.
    $recipientEmail = Fiber::suspend('waiting_for_connection');

    // --- FIBER RESUMES: runs when $emailSenderFiber->resume($value) is called ---
    // $recipientEmail now holds whatever value was passed into ->resume()
    echo "[Fiber] Resumed! Sending email to: {$recipientEmail}" . PHP_EOL;

    Fiber::suspend('sending_email');

    echo "[Fiber] Email sent. Fiber finishing." . PHP_EOL;

    // The return value of the fiber callable becomes available via ->getReturn()
    return 'email_delivered';
});

// --- OUTSIDE THE FIBER ---
// Fiber hasn't started yet. isStarted() === false
echo "Fiber created. Started: " . ($emailSenderFiber->isStarted() ? 'yes' : 'no') . PHP_EOL;

// ->start() begins execution inside the Fiber until the first Fiber::suspend()
// The value passed to Fiber::suspend() is returned by ->start()
$statusAfterStart = $emailSenderFiber->start();
echo "Fiber suspended. Status received: {$statusAfterStart}" . PHP_EOL;
echo "Is suspended: " . ($emailSenderFiber->isSuspended() ? 'yes' : 'no') . PHP_EOL;

// ->resume($value) passes a value INTO the fiber (it becomes the return of Fiber::suspend())
// and runs until the next suspension or completion
$statusAfterFirstResume = $emailSenderFiber->resume('alice@example.com');
echo "Fiber suspended again. Status received: {$statusAfterFirstResume}" . PHP_EOL;

// Resume with no value — the Fiber will finish (no more Fiber::suspend() calls)
$emailSenderFiber->resume();

// Now we can retrieve the Fiber's return value
echo "Fiber finished. Return value: " . $emailSenderFiber->getReturn() . PHP_EOL;
echo "Is terminated: " . ($emailSenderFiber->isTerminated() ? 'yes' : 'no') . PHP_EOL;
▶ Output
Fiber created. Started: no
[Fiber] Starting: connecting to mail server...
Fiber suspended. Status received: waiting_for_connection
Is suspended: yes
[Fiber] Resumed! Sending email to: alice@example.com
Fiber suspended again. Status received: sending_email
[Fiber] Email sent. Fiber finishing.
Fiber finished. Return value: email_delivered
Is terminated: yes
🔥
The Two-Way Channel:Fibers have a two-way communication channel that most devs miss. `Fiber::suspend($value)` sends data OUT to the caller, and `$fiber->resume($value)` sends data IN to the fiber (which becomes the return value of `Fiber::suspend()`). Think of it as a walkie-talkie: you can talk both ways, but only one party speaks at a time. This bidirectional flow is what makes Fibers powerful enough to drive generators, async I/O, and middleware pipelines.

Building a Real Concurrent Task Scheduler with Fibers

A single Fiber in isolation isn't very useful. The real power emerges when you build a scheduler — a piece of code that manages multiple Fibers, decides which one runs next, and orchestrates their suspension and resumption. This is the pattern that every serious async PHP library uses at its core.

The scheduler below simulates concurrent HTTP API calls. In a real implementation you'd use non-blocking stream wrappers or an event loop library. Here we simulate the I/O wait with usleep to demonstrate the scheduling logic cleanly, then show you the pattern you'd replace it with.

The critical insight: the scheduler sits in a loop, iterates over all pending Fibers, starts or resumes each one, and the Fiber suspends itself voluntarily after initiating its I/O. The scheduler then moves to the next Fiber. When the I/O would be ready (checked via stream_select in a real loop), the scheduler resumes the appropriate Fiber with the response data.

This is cooperative concurrency — every Fiber must be a good citizen and suspend itself promptly after starting an I/O operation. A Fiber that does CPU work for 500ms without suspending will block every other Fiber for those 500ms.

fiber_task_scheduler.php · PHP
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
<?php

// A minimal but realistic Fiber-based task scheduler.
// Demonstrates how event loops like ReactPHP and Amp work under the hood.

class TaskScheduler
{
    /** @var Fiber[] Fibers that have not started yet */
    private array $pendingFibers = [];

    /** @var Fiber[] Fibers that are suspended and waiting to be resumed */
    private array $suspendedFibers = [];

    public function addTask(Fiber $fiber): void
    {
        $this->pendingFibers[] = $fiber;
    }

    public function run(): void
    {
        // Keep running until there's nothing left to do
        while (!empty($this->pendingFibers) || !empty($this->suspendedFibers)) {

            // Start all fibers that haven't begun yet
            foreach ($this->pendingFibers as $index => $fiber) {
                $fiber->start();  // runs until first Fiber::suspend()

                if ($fiber->isSuspended()) {
                    // Fiber yielded control — move it to the suspended queue
                    $this->suspendedFibers[] = $fiber;
                }
                // If the fiber completed without suspending, it just falls off
                unset($this->pendingFibers[$index]);
            }
            $this->pendingFibers = [];

            // Resume each suspended fiber once per scheduler tick
            // In a real event loop this is where you'd check stream_select()
            $stillSuspended = [];
            foreach ($this->suspendedFibers as $fiber) {
                if ($fiber->isSuspended()) {
                    $fiber->resume(); // let it run until next suspension or completion
                }

                if ($fiber->isSuspended()) {
                    // Still not done — keep it in the queue
                    $stillSuspended[] = $fiber;
                }
            }
            $this->suspendedFibers = $stillSuspended;
        }
    }
}

// Simulates an async HTTP API call that takes varying amounts of "time"
function simulateApiRequest(string $apiName, int $simulatedDelayTicks): void
{
    echo "[{$apiName}] Request initiated — suspending while waiting for response..." . PHP_EOL;

    // Each suspend() simulates one "tick" of I/O wait.
    // In production this would be a non-blocking socket read + stream_select.
    for ($tick = 0; $tick < $simulatedDelayTicks; $tick++) {
        Fiber::suspend(); // Yield control back to the scheduler
    }

    echo "[{$apiName}] Response received after {$simulatedDelayTicks} ticks!" . PHP_EOL;
}

$scheduler = new TaskScheduler();

// Task A: slow API — 3 ticks to respond
$scheduler->addTask(new Fiber(function () {
    simulateApiRequest('WeatherAPI', 3);
    echo "[WeatherAPI] Processing weather data..." . PHP_EOL;
}));

// Task B: fast API — 1 tick to respond
$scheduler->addTask(new Fiber(function () {
    simulateApiRequest('CurrencyAPI', 1);
    echo "[CurrencyAPI] Processing exchange rates..." . PHP_EOL;
}));

// Task C: medium API — 2 ticks to respond
$scheduler->addTask(new Fiber(function () {
    simulateApiRequest('GeoLocationAPI', 2);
    echo "[GeoLocationAPI] Processing location data..." . PHP_EOL;
}));

echo "=== Scheduler starting — watch how tasks interleave ===" . PHP_EOL;
$scheduler->run();
echo "=== All tasks complete ===" . PHP_EOL;
▶ Output
=== Scheduler starting — watch how tasks interleave ===
[WeatherAPI] Request initiated — suspending while waiting for response...
[CurrencyAPI] Request initiated — suspending while waiting for response...
[GeoLocationAPI] Request initiated — suspending while waiting for response...
[CurrencyAPI] Response received after 1 ticks!
[CurrencyAPI] Processing exchange rates...
[GeoLocationAPI] Response received after 2 ticks!
[GeoLocationAPI] Processing location data...
[WeatherAPI] Response received after 3 ticks!
[WeatherAPI] Processing weather data...
=== All tasks complete ===
⚠️
Pro Tip — Don't Roll Your Own Scheduler in Production:This scheduler demonstrates the mechanics, but in production use Amp v3 or ReactPHP's event loop, both of which rebuilt their internals around PHP 8.1 Fibers. They give you non-blocking DNS, HTTP clients, file I/O, timers, and proper backpressure handling. Rolling your own means re-implementing years of edge-case fixes. Use this knowledge to *understand* those libraries, not to replace them.

Fibers vs Generators vs Threads — Knowing Which Tool to Reach For

PHP developers often confuse Fibers with Generators since both use a suspend/resume model. The key difference is the call stack. A Generator can only suspend at the top level of itself — it can't call a helper function and suspend from inside that function. Fibers can suspend from anywhere in the call stack, including deeply nested function calls. This makes Fibers far more powerful for building async abstractions.

Compared to pcntl-based forking or pthreads (now largely replaced by parallel), Fibers are lightweight — no OS process or thread overhead, no shared memory concerns, no mutex hell. Creating a thousand Fibers is practical; creating a thousand processes is not. The tradeoff is that Fibers give you concurrency without parallelism. CPU-bound work (image processing, heavy math) still blocks everything. For CPU parallelism, parallel\run() is the right tool.

ReactPHP and Amp are both single-threaded event loops. Pre-Fibers, Amp used Generators as coroutines with a lot of yield boilerplate. Post-PHP 8.1, Amp v3 uses Fibers internally so you write code that looks completely synchronous but suspends transparently. That's the endgame: async behaviour without callback hell or yield noise.

fibers_vs_generators_comparison.php · PHP
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374
<?php

// =================================================================
// PART 1: Generator — can only suspend from the TOP LEVEL of itself
// =================================================================

function generatorFetchData(): Generator
{
    echo "[Generator] Fetching user..." . PHP_EOL;
    yield 'fetching_user'; // CAN suspend here — top level of generator function

    echo "[Generator] Fetching orders..." . PHP_EOL;
    yield 'fetching_orders';

    echo "[Generator] Done." . PHP_EOL;
}

// This works fine:
$gen = generatorFetchData();
$gen->current(); // runs to first yield
$gen->send(null); // runs to second yield
$gen->send(null); // runs to completion

echo str_repeat('-', 50) . PHP_EOL;

// But try to yield from a HELPER FUNCTION called by a generator...
// You need `yield from` or you can't suspend mid-helper. Painful for deep stacks.

// =================================================================
// PART 2: Fiber — CAN suspend from anywhere in the call stack
// =================================================================

// This is a deeply nested helper function — NOT a generator, just a plain function
function connectToDatabase(string $dsn): string
{
    echo "[DB Helper] Initiating connection to {$dsn}..." . PHP_EOL;

    // We can suspend FROM INSIDE this helper function!
    // A Generator cannot do this — it has no concept of "up the stack"
    Fiber::suspend('awaiting_db_connection');

    echo "[DB Helper] Connection established." . PHP_EOL;
    return 'db_connection_handle';
}

function fetchUserFromDatabase(int $userId): array
{
    // connectToDatabase is a plain function, not a generator
    // The Fiber suspend inside it still works perfectly
    $connectionHandle = connectToDatabase('mysql://localhost/myapp');

    echo "[DB Layer] Running SELECT query for user #{$userId}..." . PHP_EOL;
    Fiber::suspend('awaiting_query_result'); // suspend while query runs

    // In reality you'd read from a non-blocking socket here
    return ['id' => $userId, 'name' => 'Alice', 'email' => 'alice@example.com'];
}

$databaseFiber = new Fiber(function (): void {
    // This calls a regular function that calls another regular function
    // that suspends the fiber — impossible to do cleanly with generators
    $user = fetchUserFromDatabase(42);
    echo "[Fiber] Got user: {$user['name']} ({$user['email']})" . PHP_EOL;
});

echo "[Main] Starting database fiber..." . PHP_EOL;
$status = $databaseFiber->start();
echo "[Main] Fiber yielded: {$status} — simulating connection wait..." . PHP_EOL;

$status = $databaseFiber->resume();
echo "[Main] Fiber yielded: {$status} — simulating query wait..." . PHP_EOL;

$databaseFiber->resume();
echo "[Main] Fiber completed." . PHP_EOL;
▶ Output
[Generator] Fetching user...
[Generator] Fetching orders...
[Generator] Done.
--------------------------------------------------
[Main] Starting database fiber...
[DB Helper] Initiating connection to mysql://localhost/myapp...
[Main] Fiber yielded: awaiting_db_connection — simulating connection wait...
[DB Helper] Connection established.
[DB Layer] Running SELECT query for user #42...
[Main] Fiber yielded: awaiting_query_result — simulating query wait...
[Fiber] Got user: Alice (alice@example.com)
[Main] Fiber completed.
⚠️
Watch Out — Fibers Are NOT Parallelism:This is the most common misconception in tech interviews. Fibers run on a single OS thread. Two Fibers cannot execute PHP bytecode at the same time — ever. If you need true CPU parallelism (e.g., resizing 50 images simultaneously), use the `parallel` extension or spawn worker processes. Fibers shine for I/O-bound concurrency where you'd otherwise be blocking on network or disk waits. Conflating the two leads to architectural mistakes that are painful to undo in production.

Production Patterns, Exception Handling & Performance Implications

Fibers in production come with sharp edges that don't show up in demos. The most critical is exception propagation. If a Fiber throws an uncaught exception, it propagates to the caller at the point of ->start() or ->resume(). Your scheduler must wrap every resume in a try/catch or one failing Fiber will bring down the entire event loop.

Memory is the next concern. Each Fiber has its own stack — defaulting to 8MB on most platforms. Spawn 1,000 concurrent Fibers and you've reserved 8GB of virtual address space. PHP won't allocate all that physical RAM upfront (it's virtual memory), but on 32-bit systems or constrained containers, you'll hit the address space ceiling fast. You can tune the stack size via fiber.stack_size in php.ini (PHP 8.1+).

For pure performance benchmarking: switching between Fibers has measurable overhead (context switch + stack swap), but it's microseconds, not milliseconds. The win comes when you eliminate blocking I/O waits. A workload with 100 API calls each taking 200ms runs in ~20 seconds sequentially but ~200ms cooperatively with Fibers — a 100x improvement. CPU-bound work shows zero benefit and slight overhead.

One production gotcha that bites hard: global state and static properties are shared across all Fibers. There's no isolation. If Fiber A writes to a static cache and Fiber B reads it, you have a race condition in all-but-name. Design your Fiber workloads to be stateless or pass context explicitly.

fiber_exception_handling.php · PHP
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
<?php

// Production-grade Fiber runner with proper exception isolation.
// One Fiber failing must NOT kill the others.

class IsolatedFiberRunner
{
    /** @var array<string, Fiber> */
    private array $fibers = [];

    /** @var array<string, \Throwable> Exceptions captured from failed Fibers */
    private array $failures = [];

    /** @var array<string, mixed> Return values from successful Fibers */
    private array $results = [];

    public function register(string $taskName, Fiber $fiber): void
    {
        $this->fibers[$taskName] = $fiber;
    }

    public function runAll(): void
    {
        // Start all fibers — catch per-fiber exceptions so others aren't affected
        foreach ($this->fibers as $taskName => $fiber) {
            try {
                $fiber->start();
            } catch (\Throwable $exception) {
                // This fiber failed on startup — quarantine it
                $this->failures[$taskName] = $exception;
                unset($this->fibers[$taskName]);
                echo "[Runner] '{$taskName}' failed on start: {$exception->getMessage()}" . PHP_EOL;
            }
        }

        // Drive suspended fibers to completion
        $activeFibers = array_filter(
            $this->fibers,
            fn(Fiber $f) => $f->isSuspended()
        );

        while (!empty($activeFibers)) {
            foreach ($activeFibers as $taskName => $fiber) {
                try {
                    $fiber->resume();

                    if ($fiber->isTerminated()) {
                        // Safely collect the return value
                        $this->results[$taskName] = $fiber->getReturn();
                        echo "[Runner] '{$taskName}' completed successfully." . PHP_EOL;
                    }
                } catch (\Throwable $exception) {
                    // Fiber threw mid-execution — isolate, don't rethrow
                    $this->failures[$taskName] = $exception;
                    echo "[Runner] '{$taskName}' failed mid-run: {$exception->getMessage()}" . PHP_EOL;
                }
            }

            // Keep only fibers that are still suspended
            $activeFibers = array_filter(
                $this->fibers,
                fn(Fiber $f) => $f->isSuspended()
            );
        }
    }

    public function getResults(): array { return $this->results; }
    public function getFailures(): array { return $this->failures; }
}

$runner = new IsolatedFiberRunner();

// Task 1: succeeds after one suspension
$runner->register('fetch_config', new Fiber(function (): string {
    echo "[fetch_config] Loading remote config..." . PHP_EOL;
    Fiber::suspend();
    echo "[fetch_config] Config loaded." . PHP_EOL;
    return '{"debug": false, "max_retries": 3}';
}));

// Task 2: throws an exception mid-flight — should NOT crash other fibers
$runner->register('fetch_feature_flags', new Fiber(function (): string {
    echo "[fetch_feature_flags] Calling feature flag service..." . PHP_EOL;
    Fiber::suspend(); // suspend first to simulate async I/O
    // Simulating a transient service failure
    throw new \RuntimeException('Feature flag service returned HTTP 503');
}));

// Task 3: also succeeds — proves isolation works
$runner->register('fetch_user_prefs', new Fiber(function (): array {
    echo "[fetch_user_prefs] Fetching user preferences..." . PHP_EOL;
    Fiber::suspend();
    echo "[fetch_user_prefs] Preferences loaded." . PHP_EOL;
    return ['theme' => 'dark', 'language' => 'en-GB'];
}));

$runner->runAll();

echo PHP_EOL . "=== Results ==" . PHP_EOL;
foreach ($runner->getResults() as $taskName => $result) {
    $formatted = is_array($result) ? json_encode($result) : $result;
    echo "  {$taskName}: {$formatted}" . PHP_EOL;
}

echo PHP_EOL . "=== Failures ===" . PHP_EOL;
foreach ($runner->getFailures() as $taskName => $exception) {
    echo "  {$taskName}: " . $exception->getMessage() . PHP_EOL;
}
▶ Output
[fetch_config] Loading remote config...
[fetch_feature_flags] Calling feature flag service...
[fetch_user_prefs] Fetching user preferences...
[fetch_config] Config loaded.
[Runner] 'fetch_config' completed successfully.
[Runner] 'fetch_feature_flags' failed mid-run: Feature flag service returned HTTP 503
[fetch_user_prefs] Preferences loaded.
[Runner] 'fetch_user_prefs' completed successfully.

=== Results ==
fetch_config: {"debug": false, "max_retries": 3}
fetch_user_prefs: {"theme":"dark","language":"en-GB"}

=== Failures ===
fetch_feature_flags: Feature flag service returned HTTP 503
⚠️
Watch Out — Calling getReturn() on a Failed Fiber Throws:If a Fiber terminated due to an uncaught exception and you call `$fiber->getReturn()`, PHP throws a `FiberError`. Always check `$fiber->isTerminated()` AND wrap `getReturn()` in a try/catch, OR use a pattern like the runner above that captures exceptions at the `resume()` call site. In production Amp v3 handles this for you via its `Future` abstraction — another reason to use a battle-tested library over hand-rolled schedulers.
Feature / AspectPHP Fibers (8.1+)PHP Generatorspcntl / parallel Extension
Suspend from nested callsYes — full stackful coroutineNo — only top-level yieldN/A — separate process/thread
True parallelismNo — single-threaded cooperativeNo — single-threaded cooperativeYes — separate OS thread or process
Memory per unit~8MB virtual stack (configurable)Minimal — only generator frameMBs per process; thread stack per thread
Exception propagationPropagates to resume() call sitePropagates on next() call siteMust use IPC or shared memory
Best use caseI/O-bound async concurrencyLazy iteration, simple coroutinesCPU-bound parallel computation
Shared state riskYes — same process, same globalsYes — same processNo — isolated by default
Built into PHP coreYes — PHP 8.1+Yes — PHP 5.5+No — requires extension install
Library ecosystemAmp v3, ReactPHP 3.xAmp v2 (legacy pattern)parallel\Runtime API

🎯 Key Takeaways

  • Fibers are stackful coroutines — they can suspend from anywhere in the call stack, including deeply nested helper functions. Generators can only yield from their own top-level scope, which makes them unsuitable for transparent async abstractions.
  • Fibers provide concurrency, not parallelism. A single OS thread runs one Fiber at a time. The performance win comes from eliminating blocking I/O waits, not from doing more CPU work simultaneously.
  • Exception handling is the production gotcha most developers hit first. Exceptions from a Fiber propagate to the ->start() or ->resume() call site — wrap every resume in try/catch in your scheduler or you'll have a fragile event loop.
  • Fibers are the primitive. In production, use Amp v3 or ReactPHP which are built on Fibers and give you non-blocking I/O, timers, cancellation tokens, and backpressure handling for free. Understanding Fibers makes you a better user of those libraries, not a builder of alternatives.

⚠ Common Mistakes to Avoid

  • Mistake 1: Calling Fiber::suspend() outside a running Fiber — PHP throws a fatal FiberError: Cannot call Fiber::suspend() when not in a fiber — Fix: always check your call context. Fiber::suspend() is only valid inside the callable passed to new Fiber(...). Extract your async helper functions so they're only ever called from within a Fiber context, never from main execution or static initializers.
  • Mistake 2: Calling $fiber->getReturn() before the Fiber has terminated — PHP throws FiberError: Cannot get fiber return value before the fiber returns — Fix: always gate getReturn() behind an $fiber->isTerminated() check. Also remember: if the Fiber threw an uncaught exception, getReturn() rethrows it — wrap it in try/catch or capture the exception at the resume() call site as shown in the production pattern above.
  • Mistake 3: Treating Fibers as parallelism and putting CPU-heavy work in them expecting a speed boost — Symptom: all other Fibers freeze during the computation, event loop becomes unresponsive — Fix: Fibers only help with I/O-bound wait time. For CPU-bound work (image manipulation, cryptography, data crunching) use the parallel extension, or offload to a queue worker. A simple rule of thumb: if the work involves waiting for a network or disk response, Fibers help. If it involves your CPU burning cycles, Fibers are the wrong tool.

Interview Questions on This Topic

  • QWhat's the fundamental difference between a stackful coroutine (Fiber) and a stackless coroutine (Generator) in PHP, and why does that distinction matter when building an async library?
  • QIf a Fiber throws an uncaught exception, where does that exception surface, and how would you design a scheduler that prevents one Fiber's failure from terminating the entire event loop?
  • QYou've put a static property on a service class and you're using it for per-request caching inside Fibers. Walk me through the concurrency bug this introduces and how you'd fix it without removing the caching logic.

Frequently Asked Questions

Do PHP Fibers require any special extensions or server setup?

No — Fibers are built directly into PHP core as of PHP 8.1. There's nothing to install via PECL or compile separately. You just need PHP 8.1 or higher. The Fiber class, FiberError, and Fiber::suspend() are available out of the box in any standard PHP 8.1+ environment, including shared hosting if it's on a modern PHP version.

Can I use PHP Fibers with traditional blocking functions like file_get_contents() or mysqli_query()?

You can put them inside a Fiber, but they'll still block — the Fiber, and therefore the entire event loop, will freeze until the call completes. Fibers only provide concurrency benefits when paired with non-blocking I/O primitives (non-blocking sockets, stream_select(), or async database drivers). Libraries like Amp and ReactPHP provide async-native replacements for common blocking operations.

What happens to the Fiber's memory when it's suspended — does PHP garbage collect it?

No. A suspended Fiber's entire call stack is preserved in memory exactly as-is — every local variable, every object reference, every stack frame. The GC cannot collect anything referenced by a suspended Fiber's stack. This is by design (you need that state to resume correctly), but it means long-lived suspended Fibers can hold memory longer than you'd expect. In high-concurrency scenarios, audit what your Fiber closures capture via use or hold in locals, especially large response bodies or collections.

🔥
TheCodeForge Editorial Team Verified Author

Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.

← PreviousLaravel Events and ListenersNext →Database Transactions in PHP
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged