# HTTP client timeouts

> The default connect timeout, read timeout and retries of the Laravel HTTP client, Guzzle, Symfony HttpClient and the AWS, Google, Stripe, Twilio, Resend, OpenAI and Mailgun SDKs, measured, with the fix for each.

Source: https://boring-observability.dev/laravel/http-client-timeouts
Section: Free tools — Boring Observability
Updated: 2026-10-13
Measured data: https://boring-observability.dev/laravel/http-client-timeouts.json

---

## The defaults

Each client sent one request with its default configuration to a server that never completes the connection and to one that accepts the connection and never answers. The table gives the timeout that ended each wait, and how many attempts reached a server that answers `500` and one that closes the connection without a reply.

| Client | Connect timeout | Server never answers | Attempts on a 500 | Attempts on a dropped connection |
| --- | --- | --- | --- | --- |
| [Laravel HTTP client](#laravel) `v13.35.0` | 10 s | 30 s | 1 | 1 |
| [Guzzle](#guzzle) `7.15.5` | None (the network gave up at 76 s) | None (still waiting at 150 s) | 1 | 1 |
| [Symfony HttpClient](#symfony) `v7.4.20` | 60 s | 60 s | 1 | 1 |
| [AWS SDK for PHP](#aws) `3.401.0` | None (3 attempts, each until the network gave up) | None (still waiting at 150 s) | 3 | 3 |
| [Google API client](#google) `v2.20.1` | None (the network gave up at 76 s) | None (still waiting at 150 s) | 1 | 1 |
| [Resend](#resend) `v1.16.0` | None (the network gave up at 76 s) | None (still waiting at 150 s) | 1 | 1 |
| [Stripe](#stripe) `v17.6.0` | 30 s | 80 s | 1 | 1 |
| [Twilio](#twilio) `8.13.0` | 60 s | 60 s | 1 | 1 |
| [OpenAI (Symfony client)](#openai) `v0.15.0` | 60 s | 60 s | 1 | 1 |
| [OpenAI (Guzzle)](#openai-guzzle) `v0.15.0` | None (the network gave up at 77 s) | None (still waiting at 150 s) | 1 | 1 |
| [Mailgun (Symfony client)](#mailgun) `v4.5.2` | 60 s | 60 s | 1 | 1 |
| [Mailgun (Guzzle)](#mailgun-guzzle) `v4.5.2` | None (the network gave up at 77 s) | None (still waiting at 150 s) | 1 | 1 |

Measured 13 October 2026 on PHP 8.4.25 with cURL 8.14.1 and `default_socket_timeout` 60. A wait still going at 150 seconds was stopped there. The raw measurements are at [/laravel/http-client-timeouts.json](https://boring-observability.dev/laravel/http-client-timeouts.json).

## What a missing timeout costs

A request with no timeout holds whatever started it until the far side gives up, and a server that accepted the connection and stopped answering may never give up. In a queue worker the job holds a process until the worker's own timeout kills it. If that timeout is not below the connection's `retry_after`, the job is handed to a second worker while the first is still waiting on the same call. In a web request it holds a PHP-FPM worker. [Requests that never get a response](https://boring-observability.dev/blog/instrumenting-guzzle-laravel#requests-that-never-got-a-response) are also the ones that most logging misses, because there is no response to log.

## Client by client

### Laravel HTTP client

`illuminate/http` v13.35.0. Used by Http::get() and every Http:: call.

PendingRequest sets connect_timeout 10 and timeout 30 on every request. None unless you call retry(). A 500 is returned as a response; throw() turns it into a RequestException.

```php
Http::connectTimeout(2)->timeout(5)->get($url);

// or for every request, in a service provider:
Http::globalOptions(['connect_timeout' => 2, 'timeout' => 5]);
```

### Guzzle

`guzzlehttp/guzzle` 7.15.5. Used by new GuzzleHttp\Client, and every SDK that builds one without options.

No timeout and no connect_timeout. A request to a server that accepts and never answers waits for ever. None. A 5xx throws ServerException through the http_errors middleware.

```php
new GuzzleHttp\Client(['connect_timeout' => 2, 'timeout' => 5]);
```

### Symfony HttpClient

`symfony/http-client` v7.4.20. Used by HttpClient::create(), and the PSR-18 client discovery picks when it is installed.

timeout is an idle timeout taken from PHP's default_socket_timeout, 60 seconds unless php.ini changes it. It covers waiting to connect too. max_duration, the cap on the whole request, is 0: none. None unless you wrap it in RetryableHttpClient.

```php
HttpClient::create(['timeout' => 5, 'max_duration' => 30]);
```

### AWS SDK for PHP

`aws/aws-sdk-php` 3.401.0. Used by S3 filesystem disks, SQS queues, SES mail, DynamoDB cache.

No timeout and no connect timeout, and the default retry mode makes three attempts. A silent endpoint holds the call for ever; an unreachable one costs three full connect attempts. 3 attempts in total for a 500 and for a dropped connection.

```php
// config/filesystems.php, the s3 disk: Laravel passes the disk's
// config to the S3Client, so an 'http' key reaches it as-is.
's3' => [
    'driver' => 's3',
    // ...
    'http' => ['connect_timeout' => 2, 'timeout' => 5],
],
```

### Google API client

`google/apiclient` v2.20.1. Used by Google\Client, and every Google\Service built on it.

Builds a Guzzle client with no timeout options, so it inherits Guzzle's none. None by default (its retry config is empty).

```php
$client = new Google\Client;
$client->setHttpClient(new GuzzleHttp\Client([
    'connect_timeout' => 2,
    'timeout' => 5,
    'http_errors' => false, // the default client sets it, and the library expects it
]));
```

### Resend

`resend/resend-php` v1.16.0. Used by Laravel's resend mail transport.

Resend::client() builds a Guzzle client with no options, and takes none from you. Laravel's resend mailer calls it, so mail sent through Resend inherits no timeout. None.

```php
// AppServiceProvider::boot(): replace the transport Laravel builds.
use Illuminate\Mail\Transport\ResendTransport;
use Resend\Transporters\HttpTransporter;
use Resend\ValueObjects\ApiKey;
use Resend\ValueObjects\Transporter\{BaseUri, Headers};

Mail::extend('resend', fn (array $config) => new ResendTransport(
    new Resend\Client(new HttpTransporter(
        new GuzzleHttp\Client(['connect_timeout' => 2, 'timeout' => 5]),
        BaseUri::from('api.resend.com'),
        Headers::withAuthorization(ApiKey::from($config['key'] ?? config('services.resend.key'))),
    )),
));
```

### Stripe

`stripe/stripe-php` v17.6.0. Used by StripeClient, and Laravel Cashier through it.

Its own cURL client, with DEFAULT_CONNECT_TIMEOUT 30 and DEFAULT_TIMEOUT 80. None: max_network_retries defaults to 0.

```php
$curl = new Stripe\HttpClient\CurlClient;
$curl->setConnectTimeout(2);
$curl->setTimeout(5);
Stripe\ApiRequestor::setHttpClient($curl);

// retries are per client:
new Stripe\StripeClient(['api_key' => $key, 'max_network_retries' => 2]);
```

### Twilio

`twilio/sdk` 8.13.0. Used by Twilio\Rest\Client.

Its own cURL client, with CURLOPT_TIMEOUT 60 and no connect timeout of its own. The 60 seconds is the whole request, so it also bounds the connect. None. A 500 comes back as a response, not an exception.

```php
new Twilio\Rest\Client($sid, $token, null, null, new Twilio\Http\CurlClient([
    CURLOPT_CONNECTTIMEOUT => 2,
    CURLOPT_TIMEOUT => 5,
]));
```

### OpenAI (openai-php/client), with symfony/http-client installed

`openai-php/client` v0.15.0. Used by OpenAI::client() and OpenAI::factory().

Discovers a PSR-18 client. With symfony/http-client installed, discovery picks Symfony's, so it inherits the 60-second idle timeout. None.

```php
OpenAI::factory()
    ->withApiKey($key)
    ->withHttpClient(new GuzzleHttp\Client(['connect_timeout' => 2, 'timeout' => 30]))
    ->make();
```

### OpenAI (openai-php/client), without symfony/http-client

`openai-php/client` v0.15.0. Used by the same calls, in an app with only Guzzle.

Discovery falls back to a bare GuzzleHttp\Client, so a model that never starts answering holds the request for ever. None.

```php
OpenAI::factory()
    ->withApiKey($key)
    ->withHttpClient(new GuzzleHttp\Client(['connect_timeout' => 2, 'timeout' => 30]))
    ->make();
```

### Mailgun, with symfony/http-client installed

`mailgun/mailgun-php` v4.5.2. Used by Mailgun::create().

Discovers a PSR-18 client, so the same split as OpenAI: Symfony's 60-second idle timeout when it is installed. None.

```php
new Mailgun\Mailgun((new Mailgun\HttpClient\HttpClientConfigurator)
    ->setApiKey($key)
    ->setHttpClient(new GuzzleHttp\Client(['connect_timeout' => 2, 'timeout' => 5])));
```

### Mailgun, without symfony/http-client

`mailgun/mailgun-php` v4.5.2. Used by the same call, in an app with only Guzzle.

A bare GuzzleHttp\Client from discovery: no timeout. None.

```php
new Mailgun\Mailgun((new Mailgun\HttpClient\HttpClientConfigurator)
    ->setApiKey($key)
    ->setHttpClient(new GuzzleHttp\Client(['connect_timeout' => 2, 'timeout' => 5])));
```

## Questions

### What timeout does Laravel's HTTP client use by default?

Ten seconds to connect and thirty for the whole request. PendingRequest sets connect_timeout 10 and timeout 30 on every request, and Http::connectTimeout() and Http::timeout() change them per call. It does not retry unless you call retry().

### Does Guzzle have a default timeout?

No. A GuzzleHttp\Client created without options sets neither timeout nor connect_timeout. Against a server that accepted the connection and never answered, it was still waiting when the probe stopped it at 150 seconds. Any SDK that builds a bare Guzzle client inherits that, which includes Google's, Resend's and, in an app without symfony/http-client, OpenAI's and Mailgun's.

### Why does the connect column say the network gave up?

A client with no connect timeout of its own waits until the operating system or the network stops trying. In the probe's environment that was about 75 seconds. On your servers it may be longer or shorter, which is the point: it is not a number you chose.

### How were these measured?

Each client was installed at the version shown and sent one request with its default configuration, changing only the address through the SDK's own endpoint option. One test server never answers a connect, one accepts and goes quiet, one answers 500, and one closes the connection without a reply. The same run applies every fix on this page and checks that the request then fails in about the time the fix sets.

