Asynchronous and concurrent HTTP requests in PHP

PHP is primarily a synchronous language, meaning that each line of code is executed sequentially, with each subsequent line waiting for the previous one to complete.

In certain scenarios, this method may not be the most efficient. Consider, for example, the need to execute five independent HTTP requests to an API. In this situation, we send the first request and wait for its response, then proceed to send the second request and wait again, continuing this pattern through all five requests. This process is time-consuming and less efficient than necessary.

Here's an example where we send a regular HTTP POST request using Guzzle to the OpenAI chat endpoint to translate text from the Serbian to the English language.

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

$items = [
  'Translate to English from Serbian: Prvi korak.',
  'Translate to English from Serbian: Drugi korak.',
  'Translate to English from Serbian: Treći korak.',
  'Translate to English from Serbian: Četvrti korak.',
  'Translate to English from Serbian: Peti korak.',
];

$start_time = microtime(TRUE);

foreach ($items as $item) {
  fetch_data($item);
}

$end_time = microtime(TRUE);
$elapsed_time = number_format($end_time - $start_time, 2);
var_dump("Elapsed time: $elapsed_time seconds.");

function fetch_data($item) {
  $client = new Client([
    'base_uri' => 'https://api.openai.com/v1/',
    'headers' => [
      'Content-Type' => 'application/json',
      'Authorization' => 'Bearer YOUR_OPEN_AI_KEY',
      'OpenAI-Organization' => 'YOUR_ORGANIZATION_ID',
    ],
  ]);

  try {
    $response = $client->post('chat/completions', [
      'json' => [
        'model' => 'gpt-3.5-turbo',
        'messages' => [
          [
            'role' => 'user',
            'content' => $item,
          ],
        ],
      ],
    ]);

    $response_data = json_decode($response->getBody()->getContents(), TRUE);
    var_dump($response_data['choices'][0]['message']['content']);
  }
  catch (RequestException $e) {
    var_dump($e->getMessage());
  }
}

If you analyze the results, you will see that the order of responses is identical to the one we used when sending the requests. That's because we sent requests one after the other.

Image

And the time needed to send each HTTP request individually and to receive a response for each is about 7.5 seconds.

Let's see how we can improve this.

Guzzle async

A far more efficient method would be to send all five requests to the API endpoint simultaneously (at the same time). In PHP, there are at least two ways to accomplish this.

If the need for asynchronous functionality in the application is limited to sending HTTP requests, then using Guzzle with its asynchronous request feature is likely the best option. We can adapt the initial example to function asynchronously:

use GuzzleHttp\Client;
use GuzzleHttp\Promise;
use GuzzleHttp\Exception\RequestException;

$items = [
  'Translate to English from Serbian: Prvi korak.',
  'Translate to English from Serbian: Drugi korak.',
  'Translate to English from Serbian: Treći korak.',
  'Translate to English from Serbian: Četvrti korak.',
  'Translate to English from Serbian: Peti korak.',
];

$client = new Client([
  'base_uri' => 'https://api.openai.com/v1/',
  'headers' => [
    'Content-Type' => 'application/json',
    'Authorization' => 'Bearer YOUR_OPEN_AI_KEY',
    'OpenAI-Organization' => 'YOUR_ORGANIZATION_ID',
  ],
]);

$start_time = microtime(true);

$promises = [];
foreach ($items as $item) {
  $promises[] = $client->postAsync('chat/completions', [
    'json' => [
      'model' => 'gpt-3.5-turbo',
      'messages' => [
        [
          'role' => 'user',
          'content' => $item,
        ],
      ],
    ],
  ]);
}

// Wait for all the requests to complete.
$results = Promise\Utils::unwrap($promises);

$end_time = microtime(true);
$elapsed_time = number_format($end_time - $start_time, 2);

// Output the results after all promises have been resolved.
foreach ($results as $result) {
  try {
    $response_data = json_decode($result->getBody()->getContents(), true);
    var_dump($response_data['choices'][0]['message']['content']);
  } 
  catch (RequestException $e) {
    var_dump($e->getMessage());
  }
}

var_dump("Elapsed time: $elapsed_time seconds.");

To find out how to wait for the requests to complete, even if some of them fail, check out the official Guzzle async documentation. In our case, if even one request fails, we will get a ConnectException exception.

Image

As you can see from the attached screenshot above, when we send requests simultaneously, the total duration is reduced to just a little over one second. That is a significant improvement. 

Even though we are using an async request here, we see that the order of responses is the same as when we don't use async. This is because when you start resolving these promises, Guzzle, by default, resolves them in the order they were sent.

AMPHP

If you are making a concurrent application, where not only HTTP requests are asynchronous, but also some other tasks need to happen at the same time, then one of the options is to use the AMPHP library. Here is our example rewritten to use AMPHP:

use Amp\Future;
use Amp\Http\Client\HttpClientBuilder;
use Amp\Http\Client\Request;

$items = [
  'Translate to English from Serbian: Prvi korak.',
  'Translate to English from Serbian: Drugi korak.',
  'Translate to English from Serbian: Treći korak.',
  'Translate to English from Serbian: Četvrti korak.',
  'Translate to English from Serbian: Peti korak.',
];

$http_client = HttpClientBuilder::buildDefault();

$start_time = microtime(TRUE);

try {
  $responses = Future\await(array_map(function ($item) use ($http_client) {
    $uri = 'https://api.openai.com/v1/chat/completions';

    $data = [
      'model' => 'gpt-3.5-turbo',
      'messages' => [
        [
          'role' => 'user',
          'content' => $item,
        ],
      ],
    ];

    $request = new Request($uri, 'POST', json_encode($data));
    $request->addHeader('Content-Type', 'application/json');
    $request->addHeader('Authorization', 'Bearer YOUR_OPEN_AI_KEY');
    $request->addHeader('OpenAI-Organization', 'YOUR_ORGANIZATION_ID');
    $request->setTransferTimeout(180);
    $request->setInactivityTimeout(180);
    return \Amp\async(fn () => $http_client->request($request));
  }, $items));

  /** @var \Amp\Http\Client\Response $response */
  foreach ($responses as $response) {
    $response_data = json_decode($response->getBody()->read(), TRUE);
    var_dump($response_data['choices'][0]['message']['content']);
  }
}
catch (\Exception $e) {
  var_dump($e->getMessage());     
}

$end_time = microtime(TRUE);
$elapsed_time = number_format($end_time - $start_time, 2);
var_dump("Elapsed time: $elapsed_time seconds.");

And here is the screenshot of the results of sending requests:

Image

It's interesting that the order in which we receive responses using AMPHP is not the same as when we use Guzzle async. In AMPHP, responses to asynchronous requests are sorted by the order in which they arrive, not by the order in which the requests were sent.

Comparison

On a very small set, I conducted tests to compare the speed of HTTP requests, and based on this once again I say extremely limited testing set, it seems that Guzzle async is the best option for sending async requests.

  Guzzle Guzzle Async AMPHP
1 7.54 secs 1.29 secs 3.33 secs
2 6.89 secs 1.89 secs 2.58 secs
3 6.93 secs 1.66 secs 2.42 secs

But in any case, it's best to try all the options for your specific case and see which one proves to be the best.

Packages

To make the first example work, you have to install the following package using Composer:

composer require guzzlehttp/guzzle

For the second example, the additional guzzlehttp package is needed, which you can also install using Composer:

composer require guzzlehttp/promises

And the final example using AMPHP requires you to install the following packages:

composer require amphp/amp
composer require amphp/http-client

And that concludes our discussion. I hope this article will assist you in making your HTTP requests significantly more efficient.

About the Author

Goran Nikolovski is an experienced web and AI developer skilled in Drupal, React, and React Native. He founded this website and enjoys sharing his knowledge.