To make concurrent HTTP requests in PHP, start several independent requests before you read their response bodies. With Symfony HttpClient, create the response objects in one loop and consume them in a second. With Guzzle, start asynchronous requests and wait on their promises, or use GuzzleHttpPool to cap concurrency for a larger input set. Concurrent requests can reduce waiting on network I/O, but they do not make dependent operations safe to run together or remove API rate limits.
What concurrent requests mean in PHP
For ordinary synchronous code, a program sends a request, waits for its response, then sends the next request. If each request waits on a remote server, that sequence can spend much of its time waiting. Concurrent I/O lets several independent requests be in flight before the program consumes their results.
Concurrency is not the same as parallel CPU execution. PHP may coordinate multiple network operations while they wait; the remote service, your runtime, network, and chosen HTTP transport determine how much overlap is possible. There is no universal speedup or latency guarantee. Symfony’s documentation includes an illustrative example of 379 requests in less than half a second, but that is not an independent benchmark or a promise for your workload. Symfony HttpClient documentation
First check whether requests are independent
Requests are candidates for concurrency when no request needs another request’s result. Fetching several unrelated resources is a good fit. Creating a record and then fetching it by its returned ID is not: the second operation depends on the first. Keep ordering where the API’s behavior or your data consistency requires it.
#1 Best Overall
Choose a PHP HTTP client
| Approach | Best fit | Concurrency and handling |
|---|---|---|
| Symfony HttpClient | Symfony applications or standalone PHP code using Symfony’s client | Asynchronous by default; retain responses and consume them later. Supports streaming and transport features. |
| Guzzle promises | A fixed group of requests where you want explicit promise-based handling | Start with getAsync() or requestAsync(); use unwrap() for all-or-throw handling or settle() to inspect each outcome. |
| Guzzle Pool | A large or indeterminate iterable of requests | Set a finite concurrency limit and handle individual fulfilled and rejected callbacks. |
| curl_multi | Code that directly manages concurrent transfers through PHP’s cURL extension | Guzzle documents cURL multi as its parallel transport wrapper. The exact low-level implementation depends on your own cURL integration. |
Symfony documents HTTP/2 support when cURL or amphp/http-client is used. Its documentation also gives a default maximum of six concurrent connections per host; treat that as Symfony’s documented default, not a universal PHP or server limit. The effective ceiling depends on transport, configuration, system resources, and the destination. Symfony HttpClient documentation
Symfony HttpClient: launch, then consume
Install and configure Symfony HttpClient for your project before using this example. It demonstrates the key pattern: the first loop calls request() for each URL and keeps the returned response; the second loop reads each response. Symfony states that requests are sent in the first loop and consumed in the second.
<?php
use SymfonyComponentHttpClientHttpClient;
$client = HttpClient::create();
$urls = [
'users' => 'https://api.example.test/users',
'posts' => 'https://api.example.test/posts',
'comments' => 'https://api.example.test/comments',
];
$responses = [];
foreach ($urls as $key => $url) {
$responses[$key] = $client->request('GET', $url);
}
$results = [];
foreach ($responses as $key => $response) {
try {
// toArray() reads and JSON-decodes the response body.
$results[$key] = $response->toArray();
} catch (Throwable $e) {
$results[$key] = ['error' => $e->getMessage()];
}
}
print_r($results);
Use toArray() when the server returns JSON and you want decoded data. Use getContent() for the body as text, or another appropriate response reader for your content type. Catch errors around each response if partial results are useful: one failure should not prevent you from recording the others. Check HTTP status codes and the API’s error format before treating decoded content as success.
For a large input list, do not assume that storing every response and result is free. Process bounded batches or use a suitable streaming strategy, and respect per-host limits and remote quotas. Symfony documents a maximum concurrent per-host connection count with a default of six; do not interpret that as permission to overwhelm a service.
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #2
Guzzle promises: fixed request sets
Install Guzzle in the project before running this example. Each call to getAsync() starts an asynchronous request and returns a promise. Utils::settle() waits for all promises and gives you a state for each, making it useful when partial success matters.
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttpClient;
use GuzzleHttpPromiseUtils;
$client = new Client([
'connect_timeout' => 5,
'timeout' => 30,
]);
$promises = [
'users' => $client->getAsync('https://api.example.test/users'),
'posts' => $client->getAsync('https://api.example.test/posts'),
];
$settled = Utils::settle($promises)->wait();
$results = [];
foreach ($settled as $name => $result) {
if ($result['state'] === 'fulfilled') {
$response = $result['value'];
$body = $response->getBody()->getContents();
$results[$name] = [
'status' => $response->getStatusCode(),
'body' => $body,
];
} else {
$results[$name] = ['error' => (string) $result['reason']];
}
}
print_r($results);
The timeout values here are example settings, not a universal recommendation. Choose limits suitable for the service and your own request budget. Guzzle’s documentation distinguishes two wait patterns:
Utils::unwrap($promises)waits for the requests and throws if a promise fails. Use it when any failure should fail the group.Utils::settle($promises)->wait()waits for all and reports each as fulfilled or rejected. Use it when successful responses should be retained even if another request fails.
Preserve the keys in the promise array so results map back to the input that produced them. Inspect status codes and decode JSON deliberately; a fulfilled HTTP exchange is not necessarily a successful application-level response. Guzzle Quickstart
Guzzle Pool: bound concurrency for many requests
For thousands of URLs or an iterable whose size is not known in advance, a pool avoids starting every request at once. The concurrency option caps the number in flight. The callbacks receive the response or rejection and the request index so you can process results incrementally.
<?php
require __DIR__ . '/vendor/autoload.php';
use GuzzleHttpClient;
use GuzzleHttpPool;
use GuzzleHttpPsr7Request;
$client = new Client([
'connect_timeout' => 5,
'timeout' => 30,
]);
$urls = [
'https://api.example.test/users',
'https://api.example.test/posts',
'https://api.example.test/comments',
];
$requests = function () use ($urls) {
foreach ($urls as $url) {
yield new Request('GET', $url);
}
};
$pool = new Pool($client, $requests(), [
'concurrency' => 5,
'fulfilled' => function ($response, $index) {
printf("Request %s: HTTP %dn", $index, $response->getStatusCode());
// Process the response body here, or persist it for later work.
},
'rejected' => function ($reason, $index) {
fprintf(STDERR, "Request %s failed: %sn", $index, (string) $reason);
// Record failures; retry only if the operation is safe to repeat.
},
]);
$pool->promise()->wait();
The value 5 is a finite example, not a universal optimum. Increase or reduce it based on service quotas, latency, memory, file descriptors, and the client’s transport behavior. If requests target one host, account for that host’s own connection limits. Guzzle’s Pool documentation describes the concurrency option as limiting in-flight requests. Guzzle Quickstart
When to use curl_multi
PHP’s cURL multi interface coordinates multiple cURL transfers, and Guzzle’s documentation identifies cURL multi as its parallel transport wrapper. It can be appropriate when your application already owns low-level cURL setup or needs direct control over transfer handles. For new application code, Symfony HttpClient or Guzzle generally gives a higher-level response and failure model; choose the low-level route only when its extra control is worth managing handles, completion, errors, and cleanup yourself.
The supplied evidence does not establish a comparative speed ranking among curl_multi, Symfony, and Guzzle. Select by integration and operational needs, then measure your own workload with the actual destination and concurrency settings.
Operational safeguards: errors, limits, and retries
- Bound concurrency. A finite pool or batches limit simultaneous connections and memory pressure. Do not create an unbounded promise set for a large input.
- Set timeouts. Configure connection and total request timeouts appropriate to the remote API. A timeout should produce a recorded failure, not an indefinitely blocked job.
- Inspect status and content. A completed request may have a non-success status or an API-level error body. Log enough context to identify the input and response without leaking credentials or private data.
- Keep identifiers. Preserve associative keys or indexes so out-of-order completion cannot attach a response to the wrong input.
- Retry selectively. Retry only when the operation is safe to repeat, and account for retry-after guidance or API rate limits. Retrying non-idempotent writes blindly can duplicate effects.
- Monitor local constraints. File descriptor limits, memory, runtime configuration, and network resources vary by deployment. A concurrency setting that works in development may fail under production load.
Troubleshooting concurrent PHP requests
Requests still appear sequential
Check that you start all requests before reading bodies. In Symfony, calling getContent() inside the same loop immediately after each request() defeats the dispatch-then-consume pattern. In Guzzle, make sure you use asynchronous methods and wait only after building the promise set.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchRank #4
Some requests fail while others succeed
That is normal when independent requests have separate outcomes. Use Symfony per-response exception handling or Guzzle settle() and Pool’s rejected callback to preserve partial success. Record which input failed and inspect the exception or status response.
Throughput stops improving or the destination returns errors
More concurrency can encounter per-host connection ceilings, API quotas, or local resource limits. Reduce the pool limit, batch the inputs, and honor the service’s rate-limit behavior. Symfony’s documented default of six concurrent connections per host applies to its client documentation and should not be generalized to all transports or deployments.
A fulfilled promise contains an error response
Promise fulfillment means the HTTP operation completed; it does not alone mean the API operation succeeded. Check the status code and parse the response body according to that API’s contract before marking work complete.
Retries create duplicate effects
Do not automatically repeat a request that may have changed remote state unless the API supports a safe retry mechanism, such as an idempotency key, or you have otherwise established that repeating it is safe.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Clear out junk files and repair common Windows errors3Fix the driver behind crashes, sound loss and screen glitchesOr skip the browser setup
If the concurrent jobs you are building need website screenshots, ScreenshotNeo provides a screenshot API and MCP server. It accepts one GET request for a URL and returns PNG, JPEG, WebP, or PDF. It is not a replacement for general-purpose API concurrency; use it when the task is specifically capturing pages.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for API options and response details. Cookie banners are accepted like a visitor and 60+ known consent platforms, newsletter popups, and chat widgets are removed before capture; those steps can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, with response headers reporting the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for AI agents. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up free for ScreenshotNeo.
FAQ
Can I run requests concurrently if they use different HTTP methods?
Yes, if the operations are independent and the remote API permits them. The method alone does not determine whether concurrency is safe; consider state changes, ordering, and repeatability.
Does concurrent I/O require multiple PHP worker processes?
Not necessarily. The client libraries coordinate asynchronous network operations within the application. The runtime and transport determine how that work is managed; concurrency does not imply simultaneous PHP CPU execution.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Should I use a fixed promise list or a pool?
Use promises for a manageable, known set where you want explicit outcomes. Use a Pool when input may be large or open-ended and you need a finite cap on in-flight requests.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

