In Laravel 13, how does Http::pool() send requests concurrently, what does it return when one request times out, and why can't you chain withToken() before it?
answer
- closure receives a Pool
- results keyed by position or as()
- failures returned, not thrown
- concurrency 0 means all at once
- configure each pooled request
basics
~20 sHttp::pool() runs the requests added to its Pool concurrently and returns an array keyed by position or as() name. A timed-out entry holds a ConnectionException object instead of throwing, and headers must be set on each pooled request.
solid answer
~50 sIn Laravel 13, `Http::pool()` takes a closure receiving an `Illuminate\Http\Client\Pool`; each `$pool->get(...)` registers an asynchronous request, and `pool()` drives them through Guzzle promises in one PHP process, blocking until all settle — so the wall time is about the slowest request, not the sum. Results come back in an array keyed by registration position (0, 1, 2 ...), or by the name given with `$pool->as('key')`. Nothing is thrown for a single failure: a 4xx or 5xx is a `Response` with `failed()` true, and a timeout or DNS failure is a `ConnectionException` object in its slot, so check `instanceof Throwable` first. The `concurrency` argument defaults to `0`, meaning all requests at once; pass `concurrency: 5` to cap it. `Http::withToken($t)->pool(...)` sends no token because the pool builds fresh requests from the factory — configure each `$pool->...` chain.
code
php · 23 lines<?php
use Illuminate\Http\Client\Pool;
use Illuminate\Support\Facades\Http;
$token = config('services.carrier.token');
$responses = Http::pool(fn (Pool $pool) => array_map(
fn (string $parcel) => $pool->as($parcel)
->withToken($token)
->timeout(5)
->get("https://carrier.example/api/tracking/{$parcel}"),
$parcelNumbers,
), concurrency: 5);
$statuses = [];
foreach ($responses as $parcel => $result) {
if ($result instanceof \Throwable) {
report($result); // ConnectionException: timeout, DNS, refused
} elseif ($result->successful()) {
$statuses[$parcel] = $result->json('status');
}
}go deeper
Remember that Http::pool() sends several requests at once and returns an array you index by position or by the as() name.
Explain the Pool closure, why failures arrive as values, how keys are assigned, and why configuration must go on each pooled request.
Show production care: cap concurrency for rate-limited upstreams, set per-request timeouts, and check every slot for Throwable before reading it.
Decide between pool(), batch() with defer(), queued jobs and process-based concurrency by weighing latency, durability and load on the upstream.
## How pool() works `Http::pool()` sends several requests **concurrently from one PHP process**, using Guzzle's asynchronous promises rather than threads or child processes. In Laravel 13: 1. You pass a closure that receives an `Illuminate\Http\Client\Pool`. 2. Each call on the pool — `$pool->get(...)`, or `$pool->as('key')->withToken(...)->get(...)` — creates a new **asynchronous** `PendingRequest` and registers it. The closure's return value is not used; registration is the side effect that matters, and returning an array is only a readable convention. 3. `pool()` starts the promises, keeps up to `concurrency` of them in flight, and **blocks until every one has settled**. 4. It returns a plain PHP array of results, one per request. Because the requests overlap, the wall-clock time is roughly that of the **slowest** request rather than the sum. No single request gets faster; their waits are spent side by side. ## Reading the results | What happened to the request | What its slot holds | |---|---| | 2xx response | a `Response` whose `successful()` is true | | 4xx or 5xx response | a `Response` whose `failed()` is true | | timeout, DNS failure, refused connection | a `ConnectionException` **object** | | failed status with `throw()`, or exhausted `retry()` | a `RequestException` **object** | Nothing is thrown out of `pool()` for an individual failure, so one dead request does not abort the others — but an unchecked slot can hold an exception where you expected a response. Check `$result instanceof Throwable` before calling `status()` or `json()`. Keys follow registration order (0, 1, 2 ...) unless you name requests with `$pool->as('parcel-17')`, in which case the array is keyed by those names. Each result always sits under its own request's key, whichever finished first; a `foreach` over the array may visit them in completion order, so read results by key rather than by position in the loop. ## Concurrency The second argument caps how many requests are in flight at once: `Http::pool($callback, concurrency: 5)`. In Laravel 13 it **defaults to `0`**, which the client treats as "all of them": twenty tracking lookups mean twenty simultaneous requests to the carrier. Against a rate-limited API, set a cap explicitly. ## Configuring pooled requests The `Pool` builds each request from the factory, not from the `PendingRequest` that `pool()` was called on. So `Http::withToken($t)->pool(...)` sends **no** token: the token sits on a request the pool never uses. The documentation says it directly — `pool()` cannot be chained after methods such as `withHeaders()`. Configure each pooled request instead: - chain `withToken()`, `withHeaders()`, `timeout()` or `retry()` after `$pool->as(...)`, or directly on `$pool` before the verb; - factor the shared part into a small closure that takes the `Pool` and returns a configured request; - give each request its own `timeout()`: because `pool()` waits for everyone, one hung request holds the whole pool until its own timeout fires. `retry()` works on pooled requests; the delay is handed to Guzzle as its `delay` option, and a request that exhausts its attempts lands in its slot as an exception object. ## Pool versus batch `Http::batch()` is the callback-driven sibling: - its closure receives an `Illuminate\Http\Client\Batch`; - `before()`, `progress()`, `then()`, `catch()` and `finally()` register hooks; - `concurrency(5)` caps the parallelism; - `send()` runs it now, while `defer()` runs it after the HTTP response has been sent to the user. Use `pool()` when you need every answer before continuing; use `batch()` to react per request or to push the work past the response. ## When pooling is the right tool Pooling pays off when a page or job needs several **independent** HTTP answers and spends most of its time waiting on the network: tracking statuses for every parcel in an order, rates from several services of one carrier, or lookups against several endpoints of the same API. It does not help when: - each request needs the previous response (a token call, then the call that uses the token) — those stay sequential; - the work is CPU-bound PHP rather than network waiting — the pool runs in one PHP process and does not add CPU; - the upstream enforces a strict rate limit that concurrency would simply hit sooner. For running arbitrary closures in parallel rather than HTTP requests, Laravel has separate process-based tools; `pool()` is specifically for HTTP. ## Pitfalls - Treating every slot as a `Response` and calling `->json()` on a `ConnectionException`. - Forgetting a concurrency cap and tripping the carrier's rate limit. - Leaving the 30-second default timeout on pooled requests, so one hung call stretches the whole pool. - Expecting headers set before `pool()` to reach the pooled requests.
- How is Http::batch() different from Http::pool() in Laravel?`batch()` builds an `Illuminate\Http\Client\Batch` with `before()`, `progress()`, `then()`, `catch()` and `finally()` callbacks, a `concurrency()` method, and either `send()` to run now or `defer()` to run after the response is sent. `pool()` simply returns every result once all requests settle.
- What happens to a pool's total time if one of ten pooled requests hangs?`pool()` waits for every request to settle, so the whole call lasts until that request's own timeout fires — 30 seconds by default. Setting a short `timeout()` on each pooled request bounds the pool.
- Does retry() work on a request inside Http::pool()?Yes. Pooled requests retry through the promise chain, with the delay passed to Guzzle as its `delay` option. A request that runs out of attempts ends up in its result slot as a `RequestException` or `ConnectionException` object rather than being thrown.
Sending five couriers out at once instead of one courier doing five rounds: no courier drives faster, but you are done when the slowest one returns, and a courier who gets lost is written down as a failure on his line of the list rather than stopping the others.
saying these in an interview costs you the question
- Http::withToken($t)->pool(...) applies the token to every pooled request.
- A timeout inside the pool throws and aborts the other requests.
- pool() renumbers results by completion order, so $responses[0] is the fastest reply.
- pool() keeps only a couple of requests in flight unless told otherwise.
- Pooling runs each request on its own thread or child process.