Home PHP PHP Fibers — Uncaught Exceptions Crash the Scheduler
Advanced 11 min · March 06, 2026
PHP Fibers — Async PHP

PHP Fibers — Uncaught Exceptions Crash the Scheduler

An uncaught Fiber exception via resume() kills the scheduler, freezing all concurrent requests.

N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Notes here come from systems that actually shipped.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 30 min
  • Deep production experience
  • Understanding of internals and trade-offs
  • Experience debugging complex systems
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • PHP Fibers are stackful coroutines — they can suspend from any call depth
  • Fiber::suspend() pauses execution; $fiber->resume() resumes where it stopped
  • Each fiber gets its own call stack (~8MB by default)
  • Context switch overhead is ~1µs — the real win is eliminating blocking I/O
  • Production gotcha: uncaught exceptions propagate to the resume() call site
  • Biggest mistake: treating fibers like threads — they provide concurrency, not parallelism
✦ Definition~90s read
What is PHP Fibers?

PHP Fibers are a low-level concurrency primitive introduced in PHP 8.1 that enable cooperative multitasking within a single PHP process. Unlike threads or processes, fibers are not preemptively scheduled by the OS — they voluntarily yield control via Fiber::suspend(), allowing a scheduler to resume them later.

Imagine a chef in a kitchen who can only do one thing at a time.

This solves a fundamental problem in PHP: blocking I/O operations (database queries, HTTP requests, file reads) waste CPU cycles and prevent handling other requests concurrently. Fibers let you write asynchronous code that looks synchronous, without the callback hell of promises or the overhead of process forking.

Under the hood, each fiber has its own call stack and execution context, managed entirely in userland — the PHP engine saves and restores the VM state on suspension and resumption, making fiber switching orders of magnitude cheaper than OS thread context switches (microseconds vs milliseconds).

Where fibers fit in the PHP ecosystem is nuanced. They are not a replacement for traditional request-response processing in vanilla PHP-FPM — that model already handles concurrency via multiple processes. Fibers shine in long-running PHP applications: async HTTP servers (like Amp or ReactPHP), WebSocket handlers, message queue consumers, or any scenario where you need to multiplex many I/O-bound tasks in a single process.

Compared to generators (which are one-shot coroutines that can only yield up the call stack), fibers can suspend from any depth of nested function calls, making them far more flexible for building schedulers. Compared to threads (via pthreads or parallel), fibers avoid shared-memory synchronization issues and work in environments where threads are unavailable (most shared hosting, many CI pipelines).

However, fibers are not a silver bullet — CPU-bound workloads still block the entire process, and you must manually handle exception propagation across suspension points, which is where most production bugs live.

Plain-English First

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.

Why PHP Fibers Need a Try-Catch Around Every Suspension

PHP Fibers are a concurrency primitive that allows cooperative multitasking within a single PHP process. Unlike async/await, which rewrites the call stack, a Fiber suspends execution at an explicit Fiber::suspend() call and returns control to the caller — the scheduler. The scheduler then resumes other Fibers. This is not parallelism; it's interleaved execution on one thread, with the developer controlling suspension points.

When a Fiber throws an uncaught exception, PHP destroys the Fiber and propagates the exception to the point where Fiber::start() or Fiber::resume() was called. If the scheduler does not wrap every resume in a try-catch, the exception escapes the scheduler loop, crashing the entire process. This is a critical difference from promises: a rejected promise is caught by the framework; a Fiber exception is raw and immediate.

Use Fibers when you need fine-grained control over I/O-bound tasks — HTTP requests, database queries, file reads — without the overhead of process or thread creation. The real value is in building custom schedulers for high-concurrency workloads (e.g., 10,000+ concurrent HTTP calls) where you need deterministic suspension points. But the price is that every suspension point is a potential crash vector if not guarded.

⚠ Uncaught Exceptions Are Fatal
A single uncaught exception inside any Fiber kills the entire scheduler — not just that Fiber. Wrap every Fiber::resume() in a try-catch.
📊 Production Insight
A team built a web scraper using Fibers without try-catch around resume. A single malformed HTTP response threw an exception inside a Fiber, crashing the entire scraper process mid-batch, losing all in-flight requests.
Symptom: intermittent process crashes with no error logged inside the Fiber — only a generic 'Fiber exception' at the scheduler level.
Rule: every Fiber::resume() call must be inside a try-catch; treat Fibers like raw threads — exceptions are not caught by default.
🎯 Key Takeaway
Fibers are not async/await — they are manual coroutines that require explicit exception handling.
A Fiber's uncaught exception propagates to the scheduler's resume call, not to a global handler.
Always wrap Fiber::resume() in try-catch and log the exception inside the Fiber before rethrowing or discarding.
php-fibers-async PHP Fiber Scheduler Stack Layered architecture of fiber-based concurrent task execution Application Tasks HTTP Request Handler | Database Query | File I/O Operation Fiber Layer Fiber Instance | suspend()/resume() | Exception Boundary Scheduler Core Event Loop | Task Queue | Fiber Manager Exception Handling Try-Catch Wrapper | Error Logger | Fallback Handler PHP Runtime Zend Engine | Fiber API | Memory Management THECODEFORGE.IO
thecodeforge.io
Php Fibers Async

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.

One nuance often missed: the Fiber API is intentionally low-level. You cannot cancel a suspended fiber directly — you must let it resume or destroy it. There is no built-in timeout mechanism. Any timeout must be implemented in your scheduler logic, which is why libraries like Amp provide CancellationToken.

fiber_internals_demo.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?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.
📊 Production Insight
Fibers maintain a separate call stack that's preserved across suspension.
If you run CPU work without yielding, all other fibers starve.
Rule: always yield at natural I/O boundaries, not after tight loops.
Context switch cost is ~1µs — negligible compared to blocking I/O waits.
🎯 Key Takeaway
Fibers are cooperative — one busy fiber blocks all others.
Stackful: suspend from any call depth, not just top-level.
No built-in timeout or cancellation — you must implement it in your scheduler.
The two-way channel (suspend/resume data flow) is the core primitive for async abstractions.

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.

A practical improvement: a real scheduler should use a priority queue to handle fibers with pending events first. The simple round-robin approach works for small sets but degrades when fibers have vastly different wait times.

fiber_task_scheduler.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<?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.
📊 Production Insight
A simple round-robin scheduler works but can incur latencies when fibers have mixed wait times.
Use stream_select or epoll for real non-blocking I/O — usleep simulation is only for teaching.
Rule: never block inside a fiber without yielding first — you'll freeze the entire scheduler.
🎯 Key Takeaway
The scheduler is the heart of any fiber-based async system.
Always wrap resume() in try/catch to isolate fiber failures.
In production, use Amp v3 or ReactPHP — they handle edge cases your custom scheduler won't.
Cooperative concurrency: every fiber must yield promptly.
php-fibers-async Fibers vs Generators vs Threads Concurrency models comparison for PHP task scheduling PHP Fibers Generators Concurrency Model Stackful coroutines with full call stack Stackless coroutines with limited yield Exception Handling Uncaught exceptions crash scheduler Exceptions propagate through yield chain Scheduling Control Explicit suspend()/resume() by scheduler Implicit yield() from generator itself Memory Overhead Higher per-fiber memory allocation Lower per-generator memory footprint Use Case Concurrent I/O and task scheduling Lazy iteration and simple co-routines THECODEFORGE.IO
thecodeforge.io
Php Fibers Async

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.

One more distinction: Fibers can't be serialized. You can't store a suspended fiber in a session or cache. Generators suffer the same limitation. If you need to persist state across HTTP requests, think database or queue — not fiber suspension.

fibers_vs_generators_comparison.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?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 Insight
Switching between fibers incurs a small overhead — don't create millions.
For CPU-heavy work, fibers add overhead with zero benefit.
Rule: I/O-bound → fibers; CPU-bound → parallel or processes.
Fibers cannot be serialized — you cannot persist suspended state across requests.
🎯 Key Takeaway
Fibers can suspend from any depth, generators only from their own top level.
Fibers are concurrency, not parallelism — never use them for CPU crunching.
Use parallel extension for true multi-core work.
Fibers replace the yield boilerplate of generators for async control flow.

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.

Another pattern: if you need to run code after a fiber completes (cleanup, logging), use a finally block inside the fiber closure. Don't rely on post-resume logic in the scheduler — the fiber might never resume if it throws before suspending.

fiber_exception_handling.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<?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.
📊 Production Insight
Default stack size is 8MB per fiber — a thousand fibers reserve 8GB of virtual address space.
Exception isolation is mandatory: one uncaught exception kills the entire scheduler if not caught.
Rule: wrap each resume in try/catch, not the whole loop.
Static properties are shared — you cannot use them as per-fiber cache without coordination.
🎯 Key Takeaway
Always isolate fiber exceptions at the resume() call site.
Reduce fiber.stack_size if you run thousands of fibers.
Use finally blocks inside fiber closures for cleanup — post-resume logic may never run.
For production, prefer Amp v3 — it handles exception isolation, cancellation, and backpressure.

Real-World Fiber Libraries and Production Integration

PHP 8.1 Fibers are the engine under the hood of modern async PHP libraries. The two major players are Amp v3 and ReactPHP 3.x. Both have rewritten their internals to use Fibers natively, replacing the old Generator-based coroutines.

Amp v3 (released 2023) uses Fibers transparently. You write normal synchronous-looking code, and Amp's event loop schedules your fibers. It provides Amp\async() and Amp\delay() for running concurrent tasks. Cancellation is handled via CancellationToken. The library manages a global event loop based on uv (libuv) or stream_select.

ReactPHP 3.x also adopted Fibers, but more incrementally. You can still use callbacks, or you can create Fiber-based wrappers for your concurrent operations. The ReactPHP event loop is more manual: you have to decide when to use React\EventLoop\Loop::addReadStream() vs Fiber::suspend().

When integrating fibers into an existing PHP application, the biggest challenge is the contextual boundary: fibers don't work transparently with all I/O functions. A file_get_contents() call still blocks the entire process. You need async-aware drivers for HTTP, MySQL, Redis, etc. Both Amp and ReactPHP provide those, but you must replace your traditional calls.

Another integration pattern: use fibers inside a CLI daemon or a long-running worker (like a message queue consumer). The traditional request-response model is not suitable for fibers because you'd spawn a fiber per request and lose the fiber on response end. Instead, use a persistent worker that handles many requests over its lifetime, using fibers for concurrent processing within each request batch.

Performance tip: when using Amp v3, avoid creating fibers for trivial operations. The overhead of fiber creation and context switching is real. Batch small I/O operations together, or use Amp\parallel for CPU work.

amp_fiber_example.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

// Requires amphp/amp ^3.0
// This example demonstrates production-ready fiber usage with Amp v3.

\Amp\async(function (): void {
    // These two API calls run concurrently inside a fiber
    $weatherPromise = \Amp\async(fn() => file_get_contents('https://api.weather.com/v2/current?city=London'));
    $currencyPromise = \Amp\async(fn() => file_get_contents('https://api.fx.com/v1/USD?base=GBP'));

    // Await both results — the fiber suspends here until both complete
    [$weather, $currency] = yield from [
        \Amp\HttpClient\HttpClient::default()->request('GET', 'https://api.weather.com/v2/current?city=London'),
        \Amp\HttpClient\HttpClient::default()->request('GET', 'https://api.fx.com/v1/USD?base=GBP'),
    ];

    // Both requests completed without blocking the process
    echo "Weather: {$weather->getBody()->getContents()}" . PHP_EOL;
    echo "Currency: {$currency->getBody()->getContents()}" . PHP_EOL;
});
Output
(No output shown — depends on API responses)
🔥Amp v3 vs ReactPHP 3.x: Which One?
Amp v3 is the more Fiber-native library — it uses Fibers throughout its entire API and encourages writing synchronous-looking async code. ReactPHP 3.x still exposes its callback-based event loop alongside Fiber support, which can be more flexible but requires more discipline. For new projects, start with Amp v3 unless you have legacy ReactPHP code.
📊 Production Insight
Integrating fibers into an existing traditional PHP app is not trivial — you need async replacements for every blocking call.
Rule: use a dedicated async worker (CLI daemon) to run fiber workloads, not the HTTP request cycle.
Fiber creation has overhead — don't create one per micro-operation. Batch where possible.
CancellationToken is your friend — provide a timeout to avoid indefinitely suspended fibers.
🎯 Key Takeaway
Amp v3 and ReactPHP 3.x use fibers natively — prefer them over hand-rolled schedulers.
You must replace blocking I/O calls with async alternatives for fibers to help.
Use fibers in long-running workers, not in traditional request-response scripts.
Cancel long-running fibers with CancellationToken to prevent resource leaks.

Why PHP Fibers Need a Try-Catch Around Every Suspension

Most tutorials gloss over it, but here's the hard truth: if you suspend a Fiber that hasn't started yet, PHP throws a fatal error. And if you try to resume a completed Fiber? Another fatal error. Fibers are not magical coroutines that handle edge cases for you. They're low-level primitives that require defensive coding. The biggest pitfall? Fiber suspension points inside loops. If the loop condition changes between suspensions, your Fiber might die silently. I've seen production incidents where a Fiber suspended waiting for a DB query, the DB connection dropped, and the Fiber tried to resume on an already-terminated state. PHP doesn't care about your intent. It throws. Every Fiber entry point and every suspend/resume boundary should be wrapped in a try-catch. Yes, it's boilerplate. Yes, it's worth it. One unhandled FiberError in background processing can take down your entire event loop.

safe-fiber.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

$fiber = new Fiber(function (): void {
    try {
        echo "Fiber started\n";
        Fiber::suspend();
        echo "This never runs if suspend fails\n";
    } catch (FiberError $e) {
        echo "Caught: " . $e->getMessage() . "\n";
    }
});

// Attempt to resume an unstarted Fiber
try {
    $fiber->resume();
} catch (FiberError $e) {
    echo "Resume failed: " . $e->getMessage() . "\n";
}

// Now start it properly
$fiber->start();
echo "Back in main after start\n";
$fiber->resume();
echo "Back in main after resume\n";
Output
Resume failed: Cannot call Fiber::resume() on a fiber that has not started
Fiber started
Back in main after start
Back in main after resume
⚠ Production Trap:
Never assume a Fiber's state. Check Fiber::isStarted(), Fiber::isRunning(), Fiber::isSuspended(), and Fiber::isTerminated() before any operation. In high-concurrency scenarios, race conditions on Fiber state are real. Defensive checks are cheaper than debugging a crash at 3 AM.
🎯 Key Takeaway
Treat every Fiber operation as a potential crash point — defensive try-catch is not optional, it's mandatory.

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

Here's a rule the docs won't tell you: Generators are for memory-efficient iteration. Fibers are for cooperative concurrency. Threads are for CPU-bound parallelism. Mixing them up causes pain. Generators yield values outward. Fibers suspend execution inward. That subtle difference changes everything. Use generators when you're streaming data — reading large files, processing CSV rows, paginating API responses. Use fibers when you need to pause mid-execution waiting for I/O — database queries, HTTP calls, file reads. Use threads (pthreads or parallel) only when you actually need multiple CPU cores. The mistake I see most? People wrapping fiber code inside generators. Don't. You create a recursive maze that's impossible to debug. Pick the abstraction that matches the problem. Generators are lazy lists. Fibers are pauseable functions. Threads are true parallelism. They're not interchangeable and they're not designed to be stacked.

choose-tool.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php

// Generator: lazy iteration (correct use)
function readLines(string $file): Generator
{
    $handle = fopen($file, 'r');
    while (!feof($handle)) {
        yield fgets($handle);
    }
    fclose($handle);
}

// Wrong: wrapping a Fiber inside a Generator
// Don't do this:
function badPattern(): Generator
{
    $fiber = new Fiber(function () {
        Fiber::suspend(fgets(STDIN));
        return 'done';
    });
    $fiber->start();
    yield $fiber->resume();
}

// Correct: Fiber for concurrent I/O
$fiber1 = new Fiber(function (): string {
    $result = file_get_contents('https://api.example.com/data');
    Fiber::suspend();
    return json_decode($result, true)['status'];
});

$fiber2 = new Fiber(function (): string {
    $result = file_get_contents('https://api.example.com/other');
    Fiber::suspend();
    return json_decode($result, true)['status'];
});

$fiber1->start();
$fiber2->start();
echo $fiber1->resume();
echo $fiber2->resume();
Output
OK // from first API
OK // from second API
🔥Production Pattern:
Use amphp/amp or react/promise for real-world fiber concurrency. Hand-rolling Fiber management is fine for learning but in production you want battle-tested loop integration, timeout handling, and cancellation signals.
🎯 Key Takeaway
Generators yield values outward for iteration. Fibers suspend inward for concurrency. They are not the same thing. Don't nest them.

Fibers in Practice: Building a Cooperative Task Scheduler

To truly understand PHP fibers, let's build a minimal cooperative task scheduler. This scheduler will manage multiple tasks that yield control voluntarily, demonstrating how fibers enable concurrency without parallelism.

First, define a Task class that wraps a fiber. Each task has a unique ID and a fiber that executes a generator-like function:

```php class Task { public function __construct( private int $id, private Fiber $fiber ) {}

public function getId(): int { return $this->id; } public function getFiber(): Fiber { return $this->fiber; } } ```

Next, the Scheduler maintains a queue of tasks and runs them in a round-robin fashion. It suspends a task when it yields, then resumes the next task:

```php class Scheduler { private array $tasks = []; private int $nextId = 1;

public function add(callable $fn): void { $fiber = new Fiber($fn); $task = new Task($this->nextId++, $fiber); $this->tasks[] = $task; }

public function run(): void { while (!empty($this->tasks)) { $task = array_shift($this->tasks); $fiber = $task->getFiber();

if (!$fiber->isStarted()) { $fiber->start(); } elseif ($fiber->isSuspended()) { $fiber->resume(); }

if ($fiber->isTerminated()) { echo "Task {$task->getId()} completed. "; } else { $this->tasks[] = $task; // Re-queue for next round } } } } ```

Now, create tasks that yield control using Fiber::suspend(). Each task simulates I/O by sleeping (using usleep for demonstration):

```php $scheduler = new Scheduler();

$scheduler->add(function () { echo "Task 1: Start "; Fiber::suspend(); // Yield control echo "Task 1: Resume after suspend "; usleep(100000); // Simulate I/O Fiber::suspend(); echo "Task 1: End "; });

$scheduler->add(function () { echo "Task 2: Start "; Fiber::suspend(); echo "Task 2: Resume "; usleep(50000); echo "Task 2: End "; });

$scheduler->run(); ```

Output: `` Task 1: Start Task 2: Start Task 1: Resume after suspend Task 2: Resume Task 1: End Task 2: End ``

This demonstrates cooperative multitasking: tasks voluntarily yield at suspension points, and the scheduler interleaves their execution. In a real application, Fiber::suspend() would be called inside I/O operations (e.g., HTTP requests, database queries) to free the thread for other tasks.

Key insight: Fibers are not preemptive; they rely on the task to yield. This makes them ideal for I/O-bound workloads where tasks spend most time waiting.

scheduler.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class Task {
    public function __construct(
        private int $id,
        private Fiber $fiber
    ) {}

    public function getId(): int { return $this->id; }
    public function getFiber(): Fiber { return $this->fiber; }
}

class Scheduler {
    private array $tasks = [];
    private int $nextId = 1;

    public function add(callable $fn): void {
        $fiber = new Fiber($fn);
        $task = new Task($this->nextId++, $fiber);
        $this->tasks[] = $task;
    }

    public function run(): void {
        while (!empty($this->tasks)) {
            $task = array_shift($this->tasks);
            $fiber = $task->getFiber();

            if (!$fiber->isStarted()) {
                $fiber->start();
            } elseif ($fiber->isSuspended()) {
                $fiber->resume();
            }

            if ($fiber->isTerminated()) {
                echo "Task {$task->getId()} completed.\n";
            } else {
                $this->tasks[] = $task;
            }
        }
    }
}

$scheduler = new Scheduler();

$scheduler->add(function () {
    echo "Task 1: Start\n";
    Fiber::suspend();
    echo "Task 1: Resume after suspend\n";
    usleep(100000);
    Fiber::suspend();
    echo "Task 1: End\n";
});

$scheduler->add(function () {
    echo "Task 2: Start\n";
    Fiber::suspend();
    echo "Task 2: Resume\n";
    usleep(50000);
    echo "Task 2: End\n";
});

$scheduler->run();
🔥Cooperative vs Preemptive Scheduling
📊 Production Insight
In production, never use usleep() to simulate I/O; instead, integrate with non-blocking I/O libraries (e.g., ext-uv, ReactPHP) that suspend fibers when waiting for data. The scheduler should handle exceptions gracefully to avoid crashing all tasks.
🎯 Key Takeaway
Fibers allow building a cooperative task scheduler where tasks voluntarily yield control, enabling concurrency for I/O-bound operations without the complexity of threads.

Fibers vs Swoole vs ReactPHP vs FrankenPHP

PHP fibers are a low-level concurrency primitive, but several higher-level solutions exist. Here's a comparison:

Swoole: A C extension providing coroutines, async I/O, and a full event loop. Swoole coroutines are similar to fibers but are built into the runtime and offer automatic scheduling. Swoole replaces PHP's standard functions (e.g., sleep, file_get_contents) with async versions. It's ideal for building high-performance servers but requires a non-standard PHP runtime and may have compatibility issues with some PHP extensions.

ReactPHP: An event-driven, non-blocking I/O library for PHP. It uses an event loop and promises, not fibers. ReactPHP is pure PHP and works with standard PHP, but code must be written in a callback style or using generators (via react/promise). It's mature and has many packages for HTTP, websockets, etc. However, it does not provide automatic suspension; you must manually manage callbacks.

FrankenPHP: A modern application server that combines PHP with Go's concurrency model. It uses a Go-based worker pool and can serve requests concurrently. FrankenPHP is not a fiber library; it's a server that spawns multiple PHP workers. It's easy to deploy (single binary) and works with existing PHP code without modifications. However, concurrency is achieved via multiple processes, not fibers.

PHP Fibers: Built into PHP 8.1+, fibers are a low-level primitive. They require a scheduler to manage them (like the one we built). Fibers are lightweight (no OS thread overhead) and allow you to write synchronous-looking async code. However, they are not a complete solution; you need to integrate with non-blocking I/O libraries (e.g., ext-uv, ReactPHP's event loop) to actually achieve concurrency. Fibers are best for library authors building async frameworks.

When to use what? - Use Swoole if you need maximum performance and can adopt its runtime. - Use ReactPHP if you want a pure PHP solution with a rich ecosystem. - Use FrankenPHP if you want to run existing PHP apps concurrently without code changes. - Use Fibers if you are building a custom async framework or need fine-grained control over concurrency.

Fibers are the foundation; Swoole and ReactPHP are built on top of similar concepts (coroutines and event loops). FrankenPHP is a different approach entirely.

💡Choosing the Right Tool
📊 Production Insight
In production, consider the maturity of the ecosystem. ReactPHP and Swoole have been battle-tested for years. Fibers are newer and require careful exception handling. FrankenPHP is promising for drop-in concurrency but is still evolving.
🎯 Key Takeaway
PHP fibers are a low-level primitive; Swoole, ReactPHP, and FrankenPHP offer higher-level concurrency solutions with different trade-offs in performance, compatibility, and ease of use.

Real-World Fiber Examples: HTTP Client, Streaming Parser

Let's explore two practical uses of fibers: an async HTTP client and a streaming JSON parser.

Async HTTP Client: Using fibers with a non-blocking HTTP library (e.g., amphp/http-client) allows concurrent requests without blocking. Here's a simplified example using a custom fiber-based HTTP client that suspends while waiting for a response:

class AsyncHttpClient {
    public function get(string $url): Fiber {
        return new Fiber(function () use ($url) {
            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_TIMEOUT, 10);
            // In real implementation, use non-blocking curl_multi
            $result = curl_exec($ch);
            curl_close($ch);
            return $result;
        });
    }
}
$client = new AsyncHttpClient();
$fiber1 = $client->get('https://api.example.com/data1');
$fiber2 = $client->get('https://api.example.com/data2');
$fiber1->start();
$fiber2->start();
// Later, resume and get results
$result1 = $fiber1->resume();
$result2 = $fiber2->resume();
echo $result1 . "
" . $result2;

In a real implementation, you'd use curl_multi or a non-blocking socket library to truly run requests concurrently. The fiber suspends when waiting for I/O, allowing other fibers to run.

Streaming JSON Parser: Fibers can parse a large JSON stream incrementally. For example, processing a huge JSON array from an API response without loading the entire payload into memory:

function parseJsonStream(string $stream): Fiber {
    return new Fiber(function () use ($stream) {
        $parser = new JsonParser(); // Hypothetical streaming parser
        $handle = fopen($stream, 'r');
        while (!feof($handle)) {
            $chunk = fread($handle, 4096);
            foreach ($parser->feed($chunk) as $item) {
                Fiber::suspend($item); // Yield each parsed item
            }
        }
        fclose($handle);
        return null;
    });
}
$fiber = parseJsonStream('large_data.json');
$fiber->start();
while (!$fiber->isTerminated()) {
    $item = $fiber->resume();
    if ($item !== null) {
        processItem($item); // Process one item at a time
    }
}

This pattern is memory-efficient and allows interleaving parsing with other tasks.

Both examples demonstrate how fibers enable writing sequential-looking code that is actually non-blocking and concurrent.

realworld.phpPHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AsyncHttpClient {
    public function get(string $url): Fiber {
        return new Fiber(function () use ($url) {
            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($ch, CURLOPT_TIMEOUT, 10);
            $result = curl_exec($ch);
            curl_close($ch);
            return $result;
        });
    }
}

$client = new AsyncHttpClient();
$fiber1 = $client->get('https://api.example.com/data1');
$fiber2 = $client->get('https://api.example.com/data2');

$fiber1->start();
$fiber2->start();

$result1 = $fiber1->resume();
$result2 = $fiber2->resume();

echo $result1 . "\n" . $result2;
⚠ Blocking I/O in Fibers
📊 Production Insight
For production HTTP clients, use established libraries like amphp/http-client or ReactPHP's HTTP client that integrate fibers properly. For streaming parsers, consider memory usage and error handling; fibers can help process data incrementally without blocking.
🎯 Key Takeaway
Fibers enable writing async HTTP clients and streaming parsers with synchronous-looking code, but require non-blocking I/O under the hood to actually achieve concurrency.
● Production incidentPOST-MORTEMseverity: high

Exception in a single fiber took down the entire request pipeline

Symptom
All concurrent requests stop responding after one fiber throws. Logs show no exception on the event loop — only an unresponsive process.
Assumption
The scheduler assumed fibers are isolated like threads and would not affect each other if one fails.
Root cause
The scheduler called $fiber->resume() without a try/catch. When one fiber threw an uncaught exception, the scheduler itself terminated, leaving all other fibers suspended and unmanaged.
Fix
Wrap every $fiber->resume() call in a try/catch block. Capture the exception and continue the loop. Use a runner pattern like IsolatedFiberRunner shown in the article.
Key lesson
  • Never assume fibers isolate exceptions — they propagate to the caller at the resume() site.
  • Always design your Fiber scheduler to handle per-fiber failures without crashing the whole loop.
  • Use libraries like Amp v3 that provide Future-based error handling out of the box.
Production debug guideSymptom → Action guide for common Fiber failures4 entries
Symptom · 01
Fiber never resumes after suspension
Fix
Check if the scheduler loop is still running. Verify that the fiber is not stuck in a non-suspending section. Add logging inside the fiber to trace execution.
Symptom · 02
Unexpected FiberError: Cannot get fiber return value before the fiber returns
Fix
Call getReturn() only after isTerminated() returns true. If the fiber threw, handle the exception before attempting getReturn().
Symptom · 03
Memory usage grows linearly with number of suspended fibers
Fix
Each suspended fiber holds its entire call stack. Reduce stack size via fiber.stack_size in php.ini if appropriate. Ensure fibers are not holding large local variables across long suspensions.
Symptom · 04
Fibers appear to run in parallel but cause race conditions in static properties
Fix
Fibers share the same process memory. Static properties are not isolated. Replace static caching with explicit context passing or use Fiber-local storage (not built-in — implement via WeakMap or a custom registry).
★ Quick Fiber Debug Cheat SheetInstant commands and actions for common Fiber-related problems. Copy-paste ready.
Fiber::suspend() called outside a fiber context
Immediate action
Check the call chain that led to the suspend. Move the call inside a fiber's callable.
Commands
php -r 'try { Fiber::suspend(); } catch (\Throwable $e) { echo $e->getMessage(); }'
Add a guard like `if (Fiber::getCurrent() !== null) { Fiber::suspend(); }`
Fix now
Wrap your async helper functions inside new Fiber(...)->start() before calling Fiber::suspend().
Fiber throws uncaught exception, scheduler hangs+
Immediate action
Inspect the scheduler loop — likely missing try/catch around resume().
Commands
php -r 'echo FiberError::class;' (verify class exists)
Add a try/catch around each resume() and log the exception.
Fix now
Implement a robust runner like IsolatedFiberRunner from the production patterns section.
All fibers complete but application seems slower than synchronous+
Immediate action
Check if all I/O in fibers is truly non-blocking. Blocking calls inside a fiber freeze the entire event loop.
Commands
strace -e trace=network -p $(pgrep php) to see if fibers are blocking on recv/connect
Replace file_get_contents, curl_exec, or mysqli_query with async alternatives.
Fix now
Use Amp or ReactPHP asynchronous HTTP and database clients instead of raw blocking functions.
Fibers vs Generators vs Threads
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
⚙ Quick Reference
9 commands from this guide
FileCommand / CodePurpose
fiber_internals_demo.php$emailSenderFiber = new Fiber(function (): string {How PHP Fibers Actually Work
fiber_task_scheduler.phpclass TaskSchedulerBuilding a Real Concurrent Task Scheduler with Fibers
fibers_vs_generators_comparison.phpfunction generatorFetchData(): GeneratorFibers vs Generators vs Threads
fiber_exception_handling.phpclass IsolatedFiberRunnerProduction Patterns, Exception Handling & Performance Implic
amp_fiber_example.php\Amp\async(function (): void {Real-World Fiber Libraries and Production Integration
safe-fiber.php$fiber = new Fiber(function (): void {Why PHP Fibers Need a Try-Catch Around Every Suspension
choose-tool.phpfunction readLines(string $file): GeneratorFibers vs Generators vs Threads
scheduler.phpclass Task {Fibers in Practice
realworld.phpclass AsyncHttpClient {Real-World Fiber Examples

Key takeaways

1
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.
2
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.
3
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.
4
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.
5
Memory is a real constraint
each fiber reserves ~8MB virtual stack. Tune fiber.stack_size if you need thousands of concurrent fibers.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What's the fundamental difference between a stackful coroutine (Fiber) a...
Q02SENIOR
If a Fiber throws an uncaught exception, where does that exception surfa...
Q03SENIOR
You've put a static property on a service class and you're using it for ...
Q01 of 03SENIOR

What'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?

ANSWER
A stackful coroutine (Fiber) can suspend execution from anywhere in its call stack — even from deeply nested helper functions — because it preserves the entire call stack on suspension. A stackless coroutine (Generator) can only yield from its own top-level function body; it cannot suspend from a function it calls without using yield from, which still limits the suspension to the immediate next level. This matters because async libraries need to call helper functions (like connecting to a database) that themselves need to suspend. With generators you'd have to propagate the yield through every layer, creating 'colored functions'. Fibers remove that constraint entirely — any function can suspend, and the library's internals become transparent to the user.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
Do PHP Fibers require any special extensions or server setup?
02
Can I use PHP Fibers with traditional blocking functions like file_get_contents() or mysqli_query()?
03
What happens to the Fiber's memory when it's suspended — does PHP garbage collect it?
04
Can I cancel a running fiber from outside?
N
Naren Founder & Principal Engineer

20+ years shipping production PHP systems at scale. Notes here come from systems that actually shipped.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's Advanced PHP. Mark it forged?

11 min read · try the examples if you haven't

Previous
PHP Generators
12 / 29 · Advanced PHP
Next
PHP Memory Management