PHP: Amp for Concurrent HTTP
The fiber-based async runtime for PHP. Amp v3 lets you write sync-looking code that runs concurrently, the most ergonomic PHP async model.
What you’ll learn
- Run concurrent HTTP requests in Amp without promise chains.
- Use amphp/parallel and amphp/sync for coordination.
- Compare Amp's fiber model to ReactPHP's promise model.
PHP 8.1 introduced fibers, first-class coroutines. Amp v3 builds on them to provide an async runtime where code looks synchronous but runs concurrently. No .then() chains; just await (or, in Amp, Future::await()).
For new PHP async projects, Amp is increasingly the better default than ReactPHP.
Install
composer require amphp/http-client
A concurrent fetcher
<?php
require 'vendor/autoload.php';
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;
use function Amp\async;
use function Amp\Future\await;
$client = HttpClientBuilder::buildDefault();
$urls = [];
for ($i = 1; $i <= 10; $i++) {
$urls[] = "https://practice.scrapingcentral.com/api/products?page=$i";
}
$futures = array_map(
fn(string $url) => async(function () use ($client, $url) {
$response = $client->request(new Request($url));
$body = $response->getBody()->buffer();
return json_decode($body, true);
}),
$urls,
);
$results = await($futures);
foreach ($results as $i => $data) {
echo "page " . ($i + 1) . ": " . count($data) . " items\n";
}
Read carefully. Inside async(), the code is sync-looking:
$response = $client->request(new Request($url)); // blocks the fiber, not the process
$body = $response->getBody()->buffer(); // blocks the fiber, not the process
No .then() chains. No callbacks. Just sequential code. The fiber yields to other fibers when waiting on I/O.
await() waits for all the Futures to complete and returns their results in order. Equivalent to asyncio.gather in Python or Promise.all in JavaScript.
Bounded concurrency with amphp/sync
use Amp\Sync\Semaphore;
use Amp\Sync\LocalSemaphore;
$semaphore = new LocalSemaphore(5); // max 5 concurrent
$futures = array_map(
fn(string $url) => async(function () use ($client, $url, $semaphore) {
$lock = $semaphore->acquire();
try {
$response = $client->request(new Request($url));
return json_decode($response->getBody()->buffer(), true);
} finally {
$lock->release();
}
}),
$urls,
);
$results = await($futures);
The semaphore caps in-flight work at 5. The fiber waits when no permit is available, yielding to other fibers, non-blocking from the process's perspective.
Pipelines with amphp/pipeline
For streaming work (consume items as they arrive), Amp provides Pipelines:
use Amp\Pipeline\Pipeline;
$pipeline = Pipeline::fromIterable($urls)
->concurrent(10)
->map(fn($url) => $client->request(new Request($url)))
->map(fn($response) => json_decode($response->getBody()->buffer(), true));
foreach ($pipeline as $result) {
echo count($result) . " items\n";
}
The pipeline processes items concurrently (up to 10 in flight), emitting results as they come. No need to wait for the whole batch, useful for huge URL lists with downstream processing.
Cancellation
Amp supports first-class cancellation, important for long crawls:
use Amp\CancelledException;
use Amp\DeferredCancellation;
use Amp\TimeoutCancellation;
$cancellation = new TimeoutCancellation(30); // cancel after 30s
try {
$response = $client->request(new Request($url), $cancellation);
} catch (CancelledException) {
echo "timed out";
}
Cancellation tokens propagate through nested calls. A parent operation that's cancelled cancels its children, freeing resources cleanly. ReactPHP's cancellation is more awkward, Amp wins here.
Amp vs ReactPHP
| Concern | Amp v3 | ReactPHP |
|---|---|---|
| Async style | Fibers, sync-looking code | Promises, then/catch |
| Min PHP version | 8.1 (for fibers) | 7.4+ |
| Maturity | Active, growing | Stable, widely deployed |
| Cancellation | First-class | Possible but ugly |
| Pipelines / streaming | First-class | Custom implementation |
| Ecosystem | Smaller | Larger |
For greenfield PHP async, Amp is the more pleasant model. For projects already using ReactPHP, no urgent need to migrate.
Integration with Symfony
Like ReactPHP, Amp runs alongside Symfony components:
protected function execute(InputInterface $i, OutputInterface $o): int
{
$client = HttpClientBuilder::buildDefault();
$futures = [...];
$results = await($futures);
foreach ($results as $r) {
$o->writeln($r);
}
return Command::SUCCESS;
}
Run an Amp scrape inside a Console command; the command returns when Amp's work completes. Symfony's framework code runs synchronously; Amp does the concurrency.
Worker pools, amphp/parallel
For CPU-bound work (parsing huge HTML, applying ML), Amp's parallel worker pools spawn separate PHP processes:
use Amp\Parallel\Worker\WorkerPool;
use Amp\Parallel\Worker;
$pool = Worker\workerPool();
$execution = $pool->submit(new ParseHtmlTask($html));
$result = $execution->getFuture()->await();
True parallelism (not just concurrency), N workers, M cores. Useful when your bottleneck is CPU, not I/O.
Pitfalls
- Mixing sync libraries. A blocking PDO call inside an async fiber blocks the event loop. Use amphp/postgres or amphp/mysql for non-blocking DB.
- Forgetting
async(). Calling$client->request(...)directly outside an async fiber works but doesn't compose with await(). Wrap in async() to get a Future. - Resource cleanup. Forgetting
$lock->release()or$response->getBody()->buffer()(closing the body) leaks resources. Use try/finally.
Hands-on lab
Against /api/products:
- Write an Amp scraper that fetches 30 pages concurrently.
- Add a LocalSemaphore(8) to cap parallelism.
- Use a Pipeline to consume results as they arrive.
- Compare runtime to the Symfony HttpClient and ReactPHP versions.
For batch concurrent fetches, all three are within striking distance. Amp's ergonomics shine on more complex flows, cancellation, streaming, mixed I/O types.
Quiz, check your understanding
Pass mark is 70%. Pick the best answer; you’ll see the explanation right after.