PHP Fibers & Async Programming: Beyond the Request-Response Cycle
PHP has been synchronous since birth. One request, one thread, top to bottom. PHP 8.1 introduced Fibers — cooperative multitasking primitives that let you pause and resume execution. This isn't threads. It's not parallelism. It's structured concurrency within a single thread.
What Are Fibers?
A Fiber is a block of code that can suspend itself and be resumed later by the caller. Think of it as a pausable function.
$fiber = new Fiber(function (): void {
$value = Fiber::suspend('Hello');
echo "Fiber received: $value\n";
});
$result = $fiber->start(); // Starts fiber, runs until suspend
echo "Main got: $result\n"; // "Main got: Hello"
$fiber->resume('World'); // Resumes fiber with value
// Prints: "Fiber received: World"
Execution flow:
Main: start() ──→ Fiber runs ──→ Fiber::suspend('Hello') ──→ returns to Main
Main: echo "Hello"
Main: resume('World') ──→ Fiber continues ──→ $value = 'World' ──→ echo
Why Fibers Matter
The Problem: Blocking I/O
// Traditional PHP — sequential, blocking
$user = Http::get('https://api.example.com/users/1'); // Wait 200ms
$orders = Http::get('https://api.example.com/orders/1'); // Wait 300ms
$recommendations = Http::get('https://api.example.com/rec/1'); // Wait 150ms
// Total: ~650ms (sequential)
The Solution: Concurrent I/O
With Fibers (or libraries built on them), these calls can run concurrently:
// Concurrent — all requests start immediately
// Total: ~300ms (slowest request)
Raw Fiber API
Basic Example: Lazy Generator
function lazyRange(int $start, int $end): Fiber
{
return new Fiber(function () use ($start, $end): void {
for ($i = $start; $i <= $end; $i++) {
Fiber::suspend($i);
}
});
}
$fiber = lazyRange(1, 5);
$value = $fiber->start();
while (!$fiber->isTerminated()) {
echo "$value\n";
$value = $fiber->resume();
}
Fiber States
$fiber = new Fiber(function (): string {
Fiber::suspend();
return 'done';
});
$fiber->isStarted(); // false
$fiber->isRunning(); // false
$fiber->isSuspended(); // false
$fiber->isTerminated(); // false
$fiber->start();
$fiber->isSuspended(); // true
$fiber->resume();
$fiber->isTerminated(); // true
$fiber->getReturn(); // 'done'
Practical Use: Concurrent HTTP Requests
With Laravel's HTTP Client + Pool
Laravel already provides concurrent requests without raw Fibers:
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
$responses = Http::pool(fn (Pool $pool) => [
$pool->as('user')->get('https://api.example.com/users/1'),
$pool->as('orders')->get('https://api.example.com/orders/1'),
$pool->as('recommendations')->get('https://api.example.com/rec/1'),
]);
$user = $responses['user']->json();
$orders = $responses['orders']->json();
$recommendations = $responses['recommendations']->json();
Under the hood, this uses Guzzle's async/promises (cURL multi). But Fibers enable a cleaner model for libraries to build async APIs.
With AMPHP (Fiber-Based)
AMPHP v3 is built entirely on Fibers:
composer require amphp/http-client amphp/amp
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\await;
$client = HttpClientBuilder::buildDefault();
// These run concurrently!
$futures = [
'user' => async(fn () => $client->request(new Request('https://api.example.com/users/1'))),
'orders' => async(fn () => $client->request(new Request('https://api.example.com/orders/1'))),
];
// Await all results
$userResponse = $futures['user']->await();
$ordersResponse = $futures['orders']->await();
echo $userResponse->getBody()->buffer();
The magic: async() wraps each call in a Fiber. While one request waits for I/O, the event loop switches to another Fiber. No threads, no processes — just cooperative scheduling.
Building a Simple Async Runner
class AsyncRunner
{
/** @var Fiber[] */
private array $fibers = [];
public function add(callable $task): self
{
$this->fibers[] = new Fiber($task);
return $this;
}
public function run(): array
{
$results = [];
$pending = $this->fibers;
// Start all fibers
foreach ($pending as $i => $fiber) {
$fiber->start();
}
// Resume suspended fibers until all complete
while (!empty($pending)) {
foreach ($pending as $i => $fiber) {
if ($fiber->isTerminated()) {
$results[$i] = $fiber->getReturn();
unset($pending[$i]);
} elseif ($fiber->isSuspended()) {
$fiber->resume();
}
}
// Prevent busy-waiting
if (!empty($pending)) {
usleep(1000);
}
}
return $results;
}
}
$runner = new AsyncRunner();
$runner->add(function (): string {
// Simulate async work
Fiber::suspend(); // Yield control
return 'Task 1 complete';
});
$runner->add(function (): string {
Fiber::suspend();
return 'Task 2 complete';
});
$results = $runner->run();
// ['Task 1 complete', 'Task 2 complete']
AMPHP — Fiber-Based Framework
AMPHP v3 is an async PHP framework built entirely on Fibers. It provides an event loop that drives Fiber scheduling.
composer require amphp/http-client amphp/amp
Concurrent HTTP Requests
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\Future\await;
$client = HttpClientBuilder::buildDefault();
// Each async() wraps a function in a Fiber
$futures = [
'user' => async(fn () => $client->request(new Request('https://api.example.com/users/1'))),
'orders' => async(fn () => $client->request(new Request('https://api.example.com/orders/1'))),
'posts' => async(fn () => $client->request(new Request('https://api.example.com/posts'))),
];
// await() waits for all futures to complete
$responses = await($futures);
$userData = $responses['user']->getBody()->buffer();
$ordersData = $responses['orders']->getBody()->buffer();
How it works internally:
async()creates a Fiber for each HTTP request- When a Fiber calls network I/O, it
suspend()s — yielding execution - Event loop detects I/O ready →
resume()s the corresponding Fiber - Code inside Fibers looks synchronous but executes concurrently
ReactPHP — Event-Driven Alternative
ReactPHP predates Fibers, using event loop + promises (callback-based). Since v1.x it has a Fiber adapter.
composer require react/http react/event-loop
use React\Http\Browser;
use function React\Async\await;
$browser = new Browser();
// New style: using Fibers for synchronous-looking code
$response = await($browser->get('https://api.example.com/users/1'));
echo $response->getBody();
// Concurrent
$promises = [
$browser->get('https://api.example.com/users/1'),
$browser->get('https://api.example.com/orders/1'),
];
$responses = await(React\Promise\all($promises));
AMPHP vs ReactPHP:
| Feature | AMPHP v3 | ReactPHP |
|---|---|---|
| Concurrency model | Fibers-native | Promise-based (Fiber adapter) |
| API style | Sync-looking | Promise chains or Fiber adapter |
| Maturity | Newer | Older, broader ecosystem |
| Learning curve | Easier (code looks sync) | Promise chains harder to debug |
Fibers in Laravel Octane
Laravel Octane (with Swoole or FrankenPHP) keeps the application in memory, processing requests via coroutines/fibers — no re-booting per request.
composer require laravel/octane
php artisan octane:install --server=frankenphp
Concurrent Tasks in Octane
use Laravel\Octane\Facades\Octane;
// Run 3 tasks concurrently within the same request
[$users, $orders, $stats] = Octane::concurrently([
fn () => User::active()->get(),
fn () => Order::today()->sum('total'),
fn () => Cache::get('dashboard_stats'),
]);
Octane uses Swoole coroutines (similar to Fibers) to run 3 closures concurrently. When one closure waits for the database, another runs.
Important Caveat with Octane
// ❌ DANGEROUS: Static/global state persists between requests
class BadService
{
private static array $cache = []; // Keeps values between requests!
}
// ✅ SAFE: Use request-scoped state
class GoodService
{
public function __construct(
private array $cache = [],
) {}
}
Octane doesn't reboot the app per request → static properties, singletons, global state persist between requests. This is the most common source of bugs when switching to Octane.
Fibers vs Threads vs Processes
| Feature | Fibers | Threads (pthreads) | Processes (pcntl_fork) |
|---|---|---|---|
| Concurrency model | Cooperative | Preemptive | Preemptive |
| Memory sharing | Same memory | Shared (dangerous) | Separate |
| Overhead | ~8KB per fiber | ~1MB per thread | ~10MB+ per process |
| I/O optimization | Excellent | Good | Good |
| CPU parallelism | No | Yes | Yes |
| Complexity | Low | High (locks, deadlocks) | Moderate |
| PHP version | 8.1+ | Deprecated | CLI only |
Key insight: Fibers are for I/O concurrency — waiting on network, disk, database. For CPU-heavy work (image processing, encryption, ML), use processes/queues — Fibers don't help because they don't create true parallelism.
Error Handling in Async Code
use function Amp\async;
use function Amp\Future\await;
$futures = [
'users' => async(function () {
return Http::get('https://api.example.com/users')->throw()->json();
}),
'orders' => async(function () {
return Http::get('https://api.example.com/orders')->throw()->json();
}),
];
try {
$results = await($futures);
} catch (\Exception $e) {
// Exception from ANY future bubbles up here
Log::error('Async request failed', ['error' => $e->getMessage()]);
}
Timeout handling:
use Amp\TimeoutCancellation;
$response = $client->request(
new Request('https://slow-api.example.com/data'),
new TimeoutCancellation(5), // 5 second timeout
);
When to Use Fibers
Directly: Almost Never
Fibers are a low-level primitive for library authors. You shouldn't call new Fiber() in application code — just like you don't use socket() directly when you have HTTP clients.
Indirectly (Through Libraries):
| Use Case | Solution |
|---|---|
| Concurrent HTTP calls | Http::pool() (Laravel) or AMPHP |
| Concurrent DB queries | Octane concurrently() |
| Event-driven apps | ReactPHP |
| Real-time server | Swoole/FrankenPHP + Octane |
| Background processing | Laravel Queues (still best for most cases) |
| WebSocket server | Ratchet (ReactPHP) or Swoole |
Benchmark: Sequential vs Concurrent
// Setup: 5 API calls, each taking ~200ms
// Sequential
$start = microtime(true);
for ($i = 0; $i < 5; $i++) {
Http::get("https://httpbin.org/delay/0.2");
}
$sequential = microtime(true) - $start; // ~1000ms
// Concurrent
$start = microtime(true);
Http::pool(fn (Pool $pool) => array_map(
fn ($i) => $pool->get("https://httpbin.org/delay/0.2"),
range(1, 5),
));
$concurrent = microtime(true) - $start; // ~200ms
// Result: 5x faster for I/O-bound operations
Conclusion
PHP Fibers are a foundation, not a feature you use daily. They enable libraries to provide async APIs with synchronous-looking code — no callback hell, no promise chains.
For most Laravel developers:
- Use
Http::pool()for concurrent API calls — simplest approach - Use Octane
concurrently()for concurrent tasks within a request - Use Laravel Queues for background processing — still the best solution
- Understand Fibers when debugging async libraries or reading framework source
Fibers don't replace queues. Queues handle background tasks, retries, scheduling. Fibers handle concurrent I/O within a single request. Different purposes, complementary tools.
The PHP ecosystem is moving toward async — Fibers are the engine making it happen.