Skip to content

11: Async in PHP - Promises vs Fibers

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.

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

📁 View Code Examples on GitHub

This chapter includes comprehensive async examples:

  • 01-basic-fibers.php - Introduction to PHP Fibers
  • 02-fiber-tasks.php - Practical task management with Fibers
  • 03-reactphp-server.php - Async HTTP server with ReactPHP
  • 04-promises.php - Promise patterns in PHP
  • 05-guzzle-async.php - Concurrent HTTP requests with Guzzle
  • 06-websocket-server.php - Real-time WebSocket server

Run the examples:

Terminal window
cd code/php-typescript-developers/chapter-11
composer install
php 01-basic-fibers.php
// 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
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.

PHP was designed for web requests:

1. Request comes in
2. PHP executes script (blocking is fine)
3. Response sent back
4. Process ends

Each request is independent. Blocking doesn’t matter because the process dies after response.

1. Server starts
2. Event loop runs indefinitely
3. Handles multiple requests concurrently
4. Process stays alive

Node.js must be non-blocking because one blocked operation would freeze all requests.

  1. Long-running processes (workers, daemons)
  2. Concurrent HTTP requests (calling multiple APIs)
  3. WebSocket servers (real-time communication)
  4. Queue workers (processing background jobs)
  5. Scraping/crawling (many concurrent requests)
  1. Regular web requests (Laravel, Symfony handle this fine)
  2. Database queries (connection pooling handles this)
  3. File operations (usually fast enough)
  4. Most CRUD applications

Reality: 95% of PHP applications don’t need async programming.

Fibers provide cooperative multitasking (like coroutines):

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 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, 4

Fibers are low-level - you typically don’t use them directly. Libraries build on top of them.

<?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: Finishing

ReactPHP brings Node.js-style async to PHP:

Terminal window
composer require react/http react/promise
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');
<?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:

Terminal window
php server.php
# Server stays running (like Node.js)
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));
<?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";
});

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";
});
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
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 is another async library (competitor to ReactPHP):

Terminal window
composer require amphp/http-client amphp/parallel
<?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;
<?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";
}

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.

.rr.yaml
server:
command: "php worker.php"
http:
address: 0.0.0.0:8080
workers:
num_workers: 4
worker.php
<?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'));
}

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!
<?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)
}

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();

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

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

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:

Terminal window
php artisan queue:work

Advantages:

  • Simpler than async code
  • Failed jobs can retry
  • Easy to scale (add more workers)
  • Works with traditional PHP
FeatureJavaScriptPHP TraditionalPHP Async (ReactPHP/Amp)
Event LoopBuilt-inNoneLibrary-provided
Async/AwaitNativeN/AFibers + libraries
PromisesNativeLibraryReactPHP/Amp
Non-blocking I/ODefaultNoWith libraries
Long-runningYesNo (per-request)Yes (with async libs)
ComplexityMediumLowHigh
Use CasesAll appsWeb requestsSpecific scenarios
  1. PHP is synchronous by default - This is fine for 95% of web applications
  2. Fibers (PHP 8.1+) enable cooperative multitasking (low-level building block)
  3. ReactPHP/Amp bring Node.js-style async event loops to PHP
  4. Guzzle async handles concurrent HTTP requests easily with promises
  5. Most PHP apps don’t need async - Laravel queues are often sufficient alternative
  6. Async PHP is complex - Only use for WebSockets, high-concurrency, or microservices
  7. Swoole/RoadRunner offer high-performance alternatives (10-100x faster than PHP-FPM)
  8. No automatic async like JavaScript - must explicitly use async libraries
  9. Use Laravel queues for background jobs instead of in-process async
  10. ReactPHP promises work like JavaScript promises but with PHP syntax
  11. Async PHP best for: WebSocket servers, scraping, concurrent API calls, long-running daemons
  12. Traditional PHP-FPM handles concurrency at process level, not async level

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
  1. Building WebSocket server? → Use ReactPHP/Ratchet
  2. Need high concurrency? → Consider Swoole
  3. Making many HTTP requests? → Use Guzzle async
  4. Building traditional web app? → Stick with Laravel/Symfony

Now that you understand async patterns, let’s build REST APIs with PHP.

Next Chapter: 12: REST APIs: Express.js vs PHP Native


Questions or feedback? Open an issue on GitHub