Async in PHP: Promises vs Fibers
Overview
Coming from JavaScript's async/await, you might wonder: "How does PHP handle asynchronous operations?" The answer: PHP traditionally doesn't—it's synchronous by design. However, modern PHP offers Fibers (PHP 8.1+), event loops (ReactPHP, Amp), and async frameworks that bring async capabilities to PHP.
Learning Objectives
By the end of this chapter, you'll be able to:
- ✅ Understand PHP's synchronous execution model
- ✅ Use Fibers for cooperative multitasking
- ✅ Work with ReactPHP for event-driven programming
- ✅ Implement promise-like patterns
- ✅ Handle concurrent HTTP requests
- ✅ Know when async PHP makes sense
- ✅ Use async libraries effectively
Code Examples
📁 View Code Examples on GitHub
This chapter includes comprehensive async examples:
01-basic-fibers.php- Introduction to PHP Fibers02-fiber-tasks.php- Practical task management with Fibers03-reactphp-server.php- Async HTTP server with ReactPHP04-promises.php- Promise patterns in PHP05-guzzle-async.php- Concurrent HTTP requests with Guzzle06-websocket-server.php- Real-time WebSocket server
Run the examples:
cd code/php-typescript-developers/chapter-11
composer install
php 01-basic-fibers.phpThe Fundamental Difference
JavaScript: Async by Default
// JavaScript is single-threaded with event loop
console.log('1: Start');
setTimeout(() => {
console.log('2: Async operation');
}, 0);
console.log('3: End');
// Output: 1, 3, 2 (async runs later)JavaScript event loop allows non-blocking I/O operations.
PHP: Synchronous by Default
<?php
echo "1: Start\n";
sleep(1); // Blocks execution
echo "2: After sleep\n";
echo "3: End\n";
// Output: 1, 2, 3 (sequential, blocking)PHP blocks on I/O operations by default. No event loop.
Why PHP is Synchronous
Request/Response Model
PHP was designed for web requests:
1. Request comes in
2. PHP executes script (blocking is fine)
3. Response sent back
4. Process endsEach request is independent. Blocking doesn't matter because the process dies after response.
Node.js Long-Running Process
1. Server starts
2. Event loop runs indefinitely
3. Handles multiple requests concurrently
4. Process stays aliveNode.js must be non-blocking because one blocked operation would freeze all requests.
When You Need Async in PHP
✅ Use Cases for Async PHP
- Long-running processes (workers, daemons)
- Concurrent HTTP requests (calling multiple APIs)
- WebSocket servers (real-time communication)
- Queue workers (processing background jobs)
- Scraping/crawling (many concurrent requests)
❌ You DON'T Need Async PHP For
- Regular web requests (Laravel, Symfony handle this fine)
- Database queries (connection pooling handles this)
- File operations (usually fast enough)
- Most CRUD applications
Reality: 95% of PHP applications don't need async programming.
Fibers (PHP 8.1+)
Fibers provide cooperative multitasking (like coroutines):
JavaScript Async/Await
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
async function main() {
const user = await fetchUser(1);
console.log(user);
}PHP Fibers
<?php
// Fibers allow pausing/resuming execution
$fiber = new Fiber(function(): void {
echo "1: Fiber starts\n";
Fiber::suspend(); // Pause here
echo "3: Fiber resumes\n";
});
echo "0: Before fiber\n";
$fiber->start();
echo "2: Fiber suspended\n";
$fiber->resume();
echo "4: After fiber\n";
// Output: 0, 1, 2, 3, 4Fibers are low-level - you typically don't use them directly. Libraries build on top of them.
Practical Fiber Example
<?php
class AsyncTask {
private Fiber $fiber;
public function __construct(callable $task) {
$this->fiber = new Fiber($task);
}
public function run(): mixed {
if (!$this->fiber->isStarted()) {
return $this->fiber->start();
}
return $this->fiber->resume();
}
public function isFinished(): bool {
return $this->fiber->isTerminated();
}
}
// Simulate async operation
$task1 = new AsyncTask(function() {
echo "Task 1: Starting\n";
Fiber::suspend();
echo "Task 1: Finishing\n";
return "Result 1";
});
$task2 = new AsyncTask(function() {
echo "Task 2: Starting\n";
Fiber::suspend();
echo "Task 2: Finishing\n";
return "Result 2";
});
// Interleave execution
$task1->run();
$task2->run();
$task1->run();
$task2->run();
// Output:
// Task 1: Starting
// Task 2: Starting
// Task 1: Finishing
// Task 2: FinishingReactPHP: Event Loop for PHP
ReactPHP brings Node.js-style async to PHP:
Installation
composer require react/http react/promiseJavaScript HTTP Server
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello World');
});
server.listen(3000);
console.log('Server running on port 3000');ReactPHP HTTP Server
<?php
require 'vendor/autoload.php';
use React\Http\HttpServer;
use React\Http\Message\Response;
use Psr\Http\Message\ServerRequestInterface;
$server = new HttpServer(function (ServerRequestInterface $request) {
return new Response(200, ['Content-Type' => 'text/plain'], 'Hello World');
});
$socket = new React\Socket\SocketServer('127.0.0.1:8080');
$server->listen($socket);
echo "Server running on http://127.0.0.1:8080\n";Run with:
php server.php
# Server stays running (like Node.js)Promises in PHP
JavaScript Promises
function fetchUser(id) {
return new Promise((resolve, reject) => {
fetch(`/api/users/${id}`)
.then(response => response.json())
.then(user => resolve(user))
.catch(error => reject(error));
});
}
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error(error));ReactPHP Promises
<?php
use React\Promise\Promise;
function fetchUser(int $id): Promise {
return new Promise(function ($resolve, $reject) use ($id) {
// Simulate async operation
$user = ['id' => $id, 'name' => 'Alice'];
$resolve($user);
});
}
fetchUser(1)
->then(function ($user) {
echo "User: " . $user['name'] . "\n";
})
->catch(function ($error) {
echo "Error: {$error}\n";
});Promise Chaining
JavaScript:
fetchUser(1)
.then(user => fetchPosts(user.id))
.then(posts => console.log(posts))
.catch(error => console.error(error));ReactPHP:
<?php
fetchUser(1)
->then(function ($user) {
return fetchPosts($user['id']);
})
->then(function ($posts) {
echo "Posts: " . count($posts) . "\n";
})
->catch(function ($error) {
echo "Error: {$error}\n";
});Concurrent HTTP Requests
JavaScript (Promise.all)
async function fetchMultipleUsers() {
const promises = [
fetch('/api/users/1').then(r => r.json()),
fetch('/api/users/2').then(r => r.json()),
fetch('/api/users/3').then(r => r.json()),
];
const users = await Promise.all(promises);
console.log(users);
}PHP with Guzzle (Concurrent Requests)
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
$client = new Client();
// Create promises
$promises = [
'user1' => $client->getAsync('https://api.example.com/users/1'),
'user2' => $client->getAsync('https://api.example.com/users/2'),
'user3' => $client->getAsync('https://api.example.com/users/3'),
];
// Wait for all to complete
$results = Promise\Utils::unwrap($promises);
foreach ($results as $key => $response) {
echo "{$key}: " . $response->getBody() . "\n";
}Performance:
- Sequential: 3 × 1s = 3s total
- Concurrent: ~1s total (all at once)
Amp: Alternative Async Library
Amp is another async library (competitor to ReactPHP):
Installation
composer require amphp/http-client amphp/parallelAmp HTTP Requests
<?php
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
require 'vendor/autoload.php';
// Amp uses coroutines (async functions)
$client = HttpClientBuilder::buildDefault();
$request = new Request('https://api.example.com/users/1');
$response = $client->request($request);
$body = $response->getBody()->buffer();
echo $body;Concurrent Requests with Amp
<?php
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\Promise\all;
$client = HttpClientBuilder::buildDefault();
$promises = [
$client->request(new Request('https://api.example.com/users/1')),
$client->request(new Request('https://api.example.com/users/2')),
$client->request(new Request('https://api.example.com/users/3')),
];
$responses = all($promises);
foreach ($responses as $response) {
echo $response->getBody()->buffer() . "\n";
}Async Frameworks
Swoole (High-Performance)
Swoole is a PHP extension that provides async capabilities:
<?php
$server = new Swoole\HTTP\Server("127.0.0.1", 9501);
$server->on("start", function ($server) {
echo "Swoole HTTP server started at http://127.0.0.1:9501\n";
});
$server->on("request", function ($request, $response) {
$response->header("Content-Type", "text/plain");
$response->end("Hello World\n");
});
$server->start();Performance: 10-100x faster than traditional PHP-FPM.
RoadRunner (Go-based PHP Server)
# .rr.yaml
server:
command: "php worker.php"
http:
address: 0.0.0.0:8080
workers:
num_workers: 4<?php
// worker.php
use Spiral\RoadRunner;
require 'vendor/autoload.php';
$worker = RoadRunner\Worker::create();
$psr7 = new RoadRunner\Http\PSR7Worker($worker);
while ($req = $psr7->waitRequest()) {
$psr7->respond(new Response(200, [], 'Hello World'));
}Practical Async Patterns
Pattern 1: Concurrent API Calls
Problem: Fetch data from 3 APIs sequentially (slow).
Solution: Use Guzzle async requests:
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Promise;
function fetchMultipleAPIs(): array {
$client = new Client();
$promises = [
'weather' => $client->getAsync('https://api.weather.com/current'),
'news' => $client->getAsync('https://api.news.com/latest'),
'stocks' => $client->getAsync('https://api.stocks.com/prices'),
];
$results = Promise\Utils::settle($promises)->wait();
$data = [];
foreach ($results as $key => $result) {
if ($result['state'] === 'fulfilled') {
$data[$key] = json_decode($result['value']->getBody(), true);
} else {
$data[$key] = null; // Handle error
}
}
return $data;
}
$data = fetchMultipleAPIs();
// All API calls made concurrently!Pattern 2: Background Job Processing
<?php
// Traditional (blocking)
function processOrder($order) {
sendEmail($order); // Blocks for 2s
updateInventory($order); // Blocks for 1s
notifyShipping($order); // Blocks for 1s
// Total: 4s
}
// Async (non-blocking)
use React\Promise;
function processOrderAsync($order) {
return Promise\all([
sendEmailAsync($order),
updateInventoryAsync($order),
notifyShippingAsync($order),
]);
// Total: ~2s (longest operation)
}Pattern 3: WebSocket Server
JavaScript (ws library):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', ws => {
ws.on('message', message => {
ws.send(`Echo: ${message}`);
});
});PHP (Ratchet):
<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
class Chat implements MessageComponentInterface {
public function onMessage(ConnectionInterface $from, $msg) {
$from->send("Echo: {$msg}");
}
// Other interface methods...
}
$server = IoServer::factory(
new HttpServer(
new WsServer(
new Chat()
)
),
8080
);
$server->run();When to Use Async PHP
✅ Good Use Cases
1. Microservices / API Gateway
// Aggregate data from multiple services
$data = Promise\all([
$userService->getUser($id),
$orderService->getOrders($id),
$paymentService->getPayments($id),
]);2. Real-Time Applications
- Chat servers
- Live dashboards
- WebSocket connections
3. Background Workers
- Queue processing
- Scheduled tasks
- Data synchronization
4. High-Concurrency Scenarios
- Web scraping
- Bulk API calls
- Load testing tools
❌ Bad Use Cases
1. Traditional Web Apps - Use Laravel/Symfony (they handle concurrency at the process level)
2. Simple CRUD - Async adds complexity without benefit
3. Database-Heavy Apps - Connection pooling is sufficient
Laravel Queue (Alternative to Async)
Instead of async in-process, Laravel uses job queues:
<?php
// Dispatch job to queue (non-blocking)
dispatch(new SendEmailJob($user));
// Job runs asynchronously in background worker
class SendEmailJob implements ShouldQueue {
public function handle() {
Mail::to($this->user)->send(new WelcomeEmail());
}
}Start worker:
php artisan queue:workAdvantages:
- Simpler than async code
- Failed jobs can retry
- Easy to scale (add more workers)
- Works with traditional PHP
Comparison Summary
| Feature | JavaScript | PHP Traditional | PHP Async (ReactPHP/Amp) |
|---|---|---|---|
| Event Loop | Built-in | None | Library-provided |
| Async/Await | Native | N/A | Fibers + libraries |
| Promises | Native | Library | ReactPHP/Amp |
| Non-blocking I/O | Default | No | With libraries |
| Long-running | Yes | No (per-request) | Yes (with async libs) |
| Complexity | Medium | Low | High |
| Use Cases | All apps | Web requests | Specific scenarios |
Key Takeaways
- PHP is synchronous by default - This is fine for 95% of web applications
- Fibers (PHP 8.1+) enable cooperative multitasking (low-level building block)
- ReactPHP/Amp bring Node.js-style async event loops to PHP
- Guzzle async handles concurrent HTTP requests easily with promises
- Most PHP apps don't need async - Laravel queues are often sufficient alternative
- Async PHP is complex - Only use for WebSockets, high-concurrency, or microservices
- Swoole/RoadRunner offer high-performance alternatives (10-100x faster than PHP-FPM)
- No automatic async like JavaScript - must explicitly use async libraries
- Use Laravel queues for background jobs instead of in-process async
- ReactPHP promises work like JavaScript promises but with PHP syntax
- Async PHP best for: WebSocket servers, scraping, concurrent API calls, long-running daemons
- Traditional PHP-FPM handles concurrency at process level, not async level
Practical Advice
For TypeScript Developers
Coming from Node.js:
- Don't expect everything to be async
- Traditional PHP-FPM is fine for web apps
- Use queues instead of async for background work
- ReactPHP feels like Node, but ecosystem is smaller
When to Choose Async PHP
- Building WebSocket server? → Use ReactPHP/Ratchet
- Need high concurrency? → Consider Swoole
- Making many HTTP requests? → Use Guzzle async
- Building traditional web app? → Stick with Laravel/Symfony
Next Steps
Now that you understand async patterns, let's build REST APIs with PHP.
Next Chapter: 12: REST APIs: Express.js vs PHP Native
Resources
Questions or feedback? Open an issue on GitHub