PHP
GuzzleHTTP
Asynchronous Programming
Non-Blocking Requests
HTTP Requests

How to process GuzzleHTTP async requests without blocking?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Guzzle async calls improve throughput by allowing multiple HTTP requests to be in flight at once. The common confusion is the phrase non blocking in PHP, because each PHP worker is still single request oriented and eventually must wait for completion. The practical goal is to avoid waiting per request and instead wait once for a group.

How Guzzle Async Actually Works

requestAsync returns a promise immediately. Internally, Guzzle schedules the transfer on a handler such as cURL multi, then resolves or rejects the promise later. Your code can attach callbacks and continue preparing other work before collecting all results.

php
1<?php
2
3require __DIR__ . '/vendor/autoload.php';
4
5use GuzzleHttp\Client;
6use GuzzleHttp\Promise\Utils;
7
8$client = new Client(['timeout' => 5.0]);
9
10$promises = [
11    'users' => $client->requestAsync('GET', 'https://httpbin.org/json'),
12    'status' => $client->requestAsync('GET', 'https://httpbin.org/status/200'),
13    'delay' => $client->requestAsync('GET', 'https://httpbin.org/delay/1'),
14];
15
16$results = Utils::settle($promises)->wait();
17
18foreach ($results as $key => $result) {
19    if ($result['state'] === 'fulfilled') {
20        echo "$key ok, status " . $result['value']->getStatusCode() . PHP_EOL;
21    } else {
22        echo "$key failed: " . $result['reason']->getMessage() . PHP_EOL;
23    }
24}

This is still one blocking wait, but it is much faster than three sequential waits.

Process Many Requests with Concurrency Limits

Unbounded parallelism can overload upstream services or your own worker memory. Use Each::ofLimit to cap in flight operations while still benefiting from async scheduling.

php
1<?php
2
3use GuzzleHttp\Client;
4use GuzzleHttp\Promise\Each;
5
6$client = new Client();
7$urls = [
8    'https://httpbin.org/get?i=1',
9    'https://httpbin.org/get?i=2',
10    'https://httpbin.org/get?i=3',
11    'https://httpbin.org/get?i=4',
12];
13
14$generator = function () use ($client, $urls) {
15    foreach ($urls as $url) {
16        yield function () use ($client, $url) {
17            return $client->requestAsync('GET', $url);
18        };
19    }
20};
21
22$pool = Each::ofLimit(
23    $generator(),
24    2,
25    function ($response, $index) {
26        echo "request $index done with " . $response->getStatusCode() . PHP_EOL;
27    },
28    function ($reason, $index) {
29        echo "request $index failed: " . $reason . PHP_EOL;
30    }
31);
32
33$pool->wait();

A concurrency cap protects both your application and partner APIs.

Avoid Blocking User Facing Paths

In PHP FPM, the request worker remains occupied until your script ends. Even if internal HTTP calls are async, waiting for them still delays the client response. Move heavy fan out operations to queue workers when possible.

Typical web pattern:

  • Accept request and validate data
  • Push job to queue
  • Return immediate response
  • Let worker perform Guzzle async batch and persist results

This architecture achieves true user perceived non blocking behavior.

Error Handling and Observability

Attach clear rejection handlers and record per endpoint failures. Async failures can be partial, so avoid all or nothing assumptions. Log request id, URL, status code, and elapsed time for each promise result.

php
1$promise = $client->requestAsync('GET', $url)
2    ->then(
3        function ($response) use ($url) {
4            error_log("ok $url status=" . $response->getStatusCode());
5            return $response;
6        },
7        function ($reason) use ($url) {
8            error_log("fail $url reason=" . $reason->getMessage());
9            throw $reason;
10        }
11    );

Common Pitfalls

A common mistake is calling wait() right after each requestAsync, which removes all concurrency benefits. Create all promises first, then wait once.

Another issue is no timeout policy. Without timeout and connect_timeout, one slow upstream can hold the batch far longer than expected.

A third issue is unlimited concurrency. Large fan out sets can hit file descriptor limits or trigger partner rate limits. Always define a reasonable concurrency limit and retry policy.

Finally, async inside a synchronous user request is often misunderstood as true background execution. For long tasks, use queue workers to decouple HTTP response time from external API latency.

Summary

  • Guzzle async improves throughput by overlapping network waits
  • Build promise groups first, then call one collection wait
  • Use concurrency limits to protect resources and partner services
  • Add timeout, retries, and per request logging for reliability
  • For true non blocking user flow, move long async work to background workers

Course illustration
Course illustration

All Rights Reserved.