惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

人人都是产品经理
人人都是产品经理
宝玉的分享
宝玉的分享
小众软件
小众软件
有赞技术团队
有赞技术团队
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
MyScale Blog
MyScale Blog
Engineering at Meta
Engineering at Meta
Stack Overflow Blog
Stack Overflow Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
N
Netflix TechBlog - Medium
D
Docker
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
MongoDB | Blog
MongoDB | Blog
WordPress大学
WordPress大学
J
Java Code Geeks
罗磊的独立博客
V
Visual Studio Blog
雷峰网
雷峰网
H
Help Net Security
T
The Blog of Author Tim Ferriss
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
大猫的无限游戏
大猫的无限游戏
F
Fortinet All Blogs

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
PHP Fibers in Production: 4 Real Cases Where They Beat cu...
Gabriel Anha · 2026-05-24 · via DEV Community

Fibers shipped in PHP 8.1 in November 2021. The RFC was small, the API has four methods, and most Laravel codebases still don't touch them. Open vendor/ in a fresh Laravel project and you'll find Guzzle, Symfony HttpClient, and ReactPHP all using them under the hood. Your own code? Probably none.

That's fine for a CRUD app. The moment you fan out to five HTTP services, stream a multi-gigabyte file, or wire a webhook dispatcher, the queue-or-block dichotomy gets expensive. Fibers fix a specific class of problem that queues over-solve and curl_multi under-solves. Here are four cases where they earn their keep.

What Fibers actually are (60-second mental model)

A Fiber is a coroutine. Not a thread. Not a process. One stack of execution that can pause itself and resume later, all inside the same PHP process.

The whole API:

$fiber = new Fiber(function (): void {
    echo "step 1\n";
    $resumed = Fiber::suspend("paused");  // hands control back
    echo "step 2 with: $resumed\n";
});

$out = $fiber->start();           // "step 1", returns "paused"
$fiber->resume("hello");          // "step 2 with: hello"
$fiber->isTerminated();           // true

Enter fullscreen mode Exit fullscreen mode

Fiber::suspend() is the magic. It can only be called from inside a running Fiber. Call it from the main flow and you get Fiber\FiberError: Cannot call Fiber::suspend() outside of a Fiber. The caller of start() or resume() gets back whatever you suspended with.

Two things to internalise before we go further:

  1. Fibers are cooperative. Nothing preempts them. If a Fiber doesn't call suspend(), it runs to completion and blocks the main flow.
  2. They share the same process. No GIL, no memory copy, no IPC. Globals are shared. So is the PDO connection, which is the first gotcha most people hit (more below).

Case 1: Parallel HTTP fan-out

Your checkout endpoint calls the payment gateway, the fraud service, the loyalty API, the shipping calculator, and the warehouse availability service. Five calls, ~400ms each, sequential. That's 2 seconds on the happy path.

curl_multi_exec() solves this but the API is a 20-year-old polling loop with do {} while and select-style fd watching. It works. Reading it does not bring joy.

Here's the Fiber version. The pattern: each HTTP call lives in its own Fiber. A small scheduler pumps curl_multi and resumes Fibers as their handles finish.

final class ParallelHttp
{
    /** @param array<string, string> $urls keyed by label */
    public static function get(array $urls): array
    {
        $mh = curl_multi_init();
        $handles = [];
        $fibers = [];

        foreach ($urls as $key => $url) {
            $fibers[$key] = new Fiber(function () use ($url, $mh, &$handles, $key) {
                $ch = curl_init($url);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_TIMEOUT, 5);
                curl_multi_add_handle($mh, $ch);
                $handles[$key] = $ch;

                // hand control back to the scheduler until curl says done
                Fiber::suspend();

                $body = curl_multi_getcontent($ch);
                curl_multi_remove_handle($mh, $ch);
                curl_close($ch);
                return $body;
            });
            $fibers[$key]->start();
        }

        // pump until every handle is finished
        do {
            curl_multi_exec($mh, $active);
            curl_multi_select($mh, 0.05);
        } while ($active > 0);

        curl_multi_close($mh);

        $results = [];
        foreach ($fibers as $key => $fiber) {
            $results[$key] = $fiber->resume();
        }
        return $results;
    }
}

$out = ParallelHttp::get([
    'payment'  => 'https://api.example.com/payment/check',
    'fraud'    => 'https://api.example.com/fraud/score',
    'loyalty'  => 'https://api.example.com/loyalty/balance',
    'shipping' => 'https://api.example.com/shipping/quote',
    'stock'    => 'https://api.example.com/warehouse/stock',
]);

Enter fullscreen mode Exit fullscreen mode

Numbers from a synthetic benchmark against five endpoints with an artificial 400ms server-side delay: sequential cURL takes ~2,050ms wall-clock; the Fiber + curl_multi version finishes in ~440ms. That's the math you'd expect. Wall-clock collapses to roughly the slowest call plus a small scheduling overhead. The Guzzle async docs report similar shapes and ReactPHP's HTTP client benchmarks sit in the same range.

The Fiber version isn't faster than raw curl_multi. It's the same network primitive underneath. What changes is that each Fiber gets a normal-looking function body. No callbacks, no then(), no promise chains. That matters when the failure handling gets non-trivial. Try/catch works exactly the way you'd expect.

Case 2: Stream processing with backpressure

Reading a 10GB CSV with fopen + fgetcsv works fine until you batch-write to a database that can't keep up. The naive script reads as fast as PHP can parse and the downstream consumer either OOMs the queue or drops rows.

Fibers let the producer pause when the consumer is behind. Here's a pattern with a bounded buffer:

final class BoundedChannel
{
    /** @var list<array<string, string>> */
    private array $buffer = [];
    private ?Fiber $waitingProducer = null;
    private ?Fiber $waitingConsumer = null;
    private bool $closed = false;

    public function __construct(private readonly int $capacity = 100) {}

    public function send(array $row): void
    {
        while (count($this->buffer) >= $this->capacity) {
            $this->waitingProducer = Fiber::getCurrent();
            Fiber::suspend(); // wait for the consumer to drain
        }
        $this->buffer[] = $row;
        if ($this->waitingConsumer !== null) {
            $c = $this->waitingConsumer;
            $this->waitingConsumer = null;
            $c->resume();
        }
    }

    public function receive(): ?array
    {
        while ($this->buffer === [] && !$this->closed) {
            $this->waitingConsumer = Fiber::getCurrent();
            Fiber::suspend();
        }
        if ($this->buffer === []) return null;
        $row = array_shift($this->buffer);
        if ($this->waitingProducer !== null) {
            $p = $this->waitingProducer;
            $this->waitingProducer = null;
            $p->resume();
        }
        return $row;
    }

    public function close(): void { $this->closed = true; }
}

Enter fullscreen mode Exit fullscreen mode

A real ingestion pipeline wires a producer Fiber and a consumer Fiber against the channel:

$channel = new BoundedChannel(capacity: 500);

$producer = new Fiber(function () use ($channel): void {
    $fp = fopen('orders-2026.csv', 'r');
    $header = fgetcsv($fp);
    while (($row = fgetcsv($fp)) !== false) {
        $channel->send(array_combine($header, $row));
    }
    fclose($fp);
    $channel->close();
});

$consumer = new Fiber(function () use ($channel): void {
    $batch = [];
    while (($row = $channel->receive()) !== null) {
        $batch[] = $row;
        if (count($batch) === 50) {
            OrderImporter::insertBatch($batch); // blocking DB write
            $batch = [];
        }
    }
    if ($batch !== []) OrderImporter::insertBatch($batch);
});

$producer->start();
$consumer->start();

Enter fullscreen mode Exit fullscreen mode

The producer parks itself once the buffer hits 500 rows. The consumer drains, the producer resumes. Memory stays flat. A 9GB CSV with this pattern keeps resident memory under 100MB across the run, because the buffer caps at 500 rows. The interesting comparison isn't speed (the DB is the bottleneck), it's that the script can't OOM regardless of how fast fgetcsv reads.

A generator gives you laziness, but generators can't yield from inside helper methods without polluting the entire call chain. Fibers can. That's the actual ergonomic win.

Case 3: Replacing generator coroutines

Before Fibers, people did this in PHP for async work:

function fetchUser(int $id): Generator {
    $raw = yield $http->get("/users/$id");        // a Promise
    $orders = yield $http->get("/orders?u=$id");
    return ['user' => $raw, 'orders' => $orders];
}

Enter fullscreen mode Exit fullscreen mode

You needed a "coroutine runner" that walked the generator, awaited each yielded promise, and pushed the result back in. The pattern works (Recoil, Amp v2 used it heavily, even Laravel Octane had flavors of it) but the signature : Generator was always wrong. The function returns array. The type system never agreed with reality.

Fibers fix this. Same logic, real return types:

public function fetchUser(int $id): UserBundle
{
    $user = $this->client->get("/users/$id");      // suspends internally
    $orders = $this->client->get("/orders?u=$id");
    return new UserBundle($user, $orders);
}

Enter fullscreen mode Exit fullscreen mode

The client suspends the current Fiber while waiting on the socket. The function reads as straight-line synchronous code. PHPStan, Psalm, IDE autocomplete: all of them get the right types. The Amp team rewrote everything around Fibers in v3 (release notes) for exactly this reason. ReactPHP shipped react/async 4.x doing the same: await() is a fiber-based primitive that lets a promise-returning library look synchronous.

If you've got a codebase with Generator-based "async" helpers from 2019, ripping them out for Fibers usually cuts 30-40% of the file in pure ceremony. No behaviour change.

Case 4: Inside ReactPHP / AMPHP without converting the whole codebase

You don't have to go all-in on an event loop to use Fibers. This is the point most "should I use Amp?" posts miss.

A Symfony Messenger handler that needs to fan out to three webhooks can pull in a Fiber-aware HTTP client locally, run a small await inside the handler, and stay otherwise vanilla:

use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\Future\awaitAll;

final class DispatchWebhooksHandler
{
    public function __invoke(OrderPlaced $event): void
    {
        $client = HttpClientBuilder::buildDefault();
        $urls = WebhookSubscriptions::for($event->orderId);

        $futures = array_map(
            fn(string $url) => async(fn() =>
                $client->request(new Request($url, 'POST', json_encode($event)))
            ),
            $urls,
        );

        [$errors, $responses] = awaitAll($futures);
        foreach ($errors as $url => $err) {
            FailedWebhook::record($url, $err->getMessage());
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

async() spawns a Fiber. awaitAll() blocks the handler until every Fiber finishes, but only this handler. The rest of your Symfony app keeps doing whatever it's doing. No event loop running globally. No "convert the whole framework to async."

The trick is that Amp's EventLoop initialises lazily on first use and shuts down cleanly when there's nothing pending. Same with react/async. You get coroutine ergonomics in one method without infecting the rest of the codebase.

Where Fibers don't help

PDO is blocking. Sticking a PDO::query() inside a Fiber doesn't make it async. The Fiber blocks the entire process until libpq returns. Same for mysqli in default mode. Same for file_get_contents() on a local file. Same for sleep().

CPU-bound work (image resizing, JSON encoding a 200MB structure, bcrypt) gets nothing from Fibers. They're for I/O wait. If the bottleneck is a tight for loop hashing things, you need pthreads, parallel, or pcntl_fork. Not coroutines.

The mental check: would this operation, in Node.js, return a Promise from a libuv-backed call? If yes, a Fiber-based wrapper will help. If no (your bottleneck is PHP itself), Fibers won't.

The big asterisk: there are async PDO replacements (amphp/postgres, amphp/mysql) that work cooperatively with Fibers. Worth knowing they exist. Worth knowing that swapping them in is rarely free. Eloquent and Doctrine both assume blocking drivers.

The error-handling gotcha

Exceptions inside Fibers behave almost like you'd expect. Almost.

$fiber = new Fiber(function (): void {
    Fiber::suspend();
    throw new RuntimeException("boom");
});

$fiber->start();
try {
    $fiber->resume(); // exception propagates HERE, not where it was thrown
} catch (RuntimeException $e) {
    // caught
}

Enter fullscreen mode Exit fullscreen mode

The exception travels to whoever called resume(). That's usually fine. The trap is Fiber::throw():

$fiber = new Fiber(function (): void {
    try {
        Fiber::suspend();
    } catch (DomainException $e) {
        // the throw() call lands inside the suspend
    }
});
$fiber->start();
$fiber->throw(new DomainException("cancelled"));

Enter fullscreen mode Exit fullscreen mode

throw() injects an exception at the suspend() point. If the Fiber doesn't catch it, the exception bubbles up to the caller of throw(). People use this for cancellation, but it's easy to set up an exception that crosses three Fibers and lose track of who actually catches it.

Rule of thumb: keep Fiber bodies short. If a Fiber needs error handling more sophisticated than try/catch around its entry point, you probably want to be returning a Result-style object across suspend() instead of throwing through it.

What this means for your codebase

Fibers are a primitive, not a feature you bolt on. The right places to reach for them: parallel I/O fan-out, backpressure-aware streaming, replacing generator-based async helpers, and embedded use inside otherwise-blocking frameworks. The wrong places: CPU work, PDO, anywhere you don't have a clear suspend-point story.

If you already ship a Laravel or Symfony app, the lowest-risk first step is Case 4: a single handler that spawns a few Fibers via Amp or react/async, runs locally, and exits. No global event loop. No infrastructure change. Measure the win, then expand.

What's the slowest fan-out in your current app, and would Case 4 fit it without a framework rewrite?


If this was useful

Fibers are the runtime primitive. The bigger question is architectural: should this fan-out live in your domain layer or in an adapter? That's where most codebases get tangled. Decoupled PHP is the architectural layer your codebase reaches for once the framework defaults stop being enough; it covers how to keep async I/O on the edges and your domain code blissfully synchronous.

Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework

Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.