Imagine your online store calls an external payment API every time a customer places an order. One day the payment provider gets slow and takes 30 seconds, or more, to respond.
Without a timeout, your app just waits. Each waiting request holds a PHP worker. Your server only has a limited number of workers, so after a few seconds they are all stuck waiting, and new customers can’t even load the homepage. One slow dependency has taken down your whole app.
So what went wrong? Was the server too small? Was the database overloaded? Was the code broken?
No. Your servers are fine. Your database is fine. Your code works perfectly. The only problem is that your app has no idea when to stop waiting.
Do you need more servers to fix this? A bigger cloud bill? A complex new architecture?
Not at all. This is one of the easiest problems to fix in backend development. You just need to teach your app one simple rule: don’t wait forever.
And what happens the next time the payment provider gets slow?
With a few small changes in how your app calls external services, it’s no longer a disaster that takes down your whole store. It becomes a small, controlled failure: a few orders get a clear “please try again” message, and everyone else keeps shopping normally.
So how do you get there? Use these 3 tips to stop that from happening:
Tip 1: Use two timeouts, one to connect and one to respond
An HTTP call can get stuck in two different places:
- Connecting: the server is down or unreachable, so your app keeps trying to open a connection.
- Waiting for the response: the connection opened, but the server takes too long to answer.
Think of it like a phone call. The connection timeout is how long you let it ring before you hang up. The request timeout is how long you stay on the line while the other person says “hold on…” before you give up.
The bad way sets no limit at all:
$response = Http::post('https://payments.example.com/charge', [
'order_id' => 123,
'amount' => 49.90,
]);If the payment API hangs, this line can block for a very long time.
The good way sets both limits:
$response = Http::connectTimeout(2)
->timeout(5)
->post('https://payments.example.com/charge', [
'order_id' => 123,
'amount' => 49.90,
]);connectTimeout(2): if the connection doesn’t open within 2 seconds, give up.timeout(5): if the full response doesn’t arrive within 5 seconds, give up.
Keep the connection timeout short. A healthy server accepts connections in milliseconds, so if it takes longer than 2 seconds, something is wrong.
Tip 2: Turn a timeout into a clear error, not a mystery
Setting a timeout is only half the job. When the time runs out, Laravel throws a ConnectionException. If you don’t handle it, your user gets a generic 500 error and you get no useful information.
Catch it and turn it into a meaningful response:
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Facades\Http;
/**
* Charges an order through the external payment API.
*
* @param int $orderId Order identifier.
* @param float $amount Amount to charge.
* @return array<string, mixed> Payment API response body.
*
* @throws PaymentFailedException When the payment API is slow or unreachable.
*/
public function charge(int $orderId, float $amount): array
{
try {
$response = Http::connectTimeout(2)
->timeout(5)
->post('https://payments.example.com/charge', [
'order_id' => $orderId,
'amount' => $amount,
]);
} catch (ConnectionException $e) {
throw new PaymentFailedException(
message: 'Payment service is unavailable: '.$e->getMessage(),
httpStatus: 503,
previous: $e,
);
}
return $response->json();
}Now, instead of hanging for 30 seconds, the customer gets an answer within 5 seconds:
{
"message": "Payment service is unavailable. Please try again in a moment."
}Why 503 Service Unavailable? It tells the client: “It’s not your fault, a service we depend on is down right now, try again later.” That’s honest, and clients and load balancers know how to handle it.
A fast, clear failure is much better than a slow, silent one. It also frees the PHP worker right away so it can serve other customers.
Tip 3: Make timeouts configurable and log how long every call takes
How do you know that 5 seconds is the right timeout?
You don’t, until you measure.
Step 1: Move the values to configuration. Don’t hardcode them:
// config/services.php
'payment' => [
'url' => env('PAYMENT_SERVICE_URL'),
'timeout' => env('PAYMENT_TIMEOUT_SECONDS', 5),
'connect_timeout' => env('PAYMENT_CONNECT_TIMEOUT_SECONDS', 2),
],# .env
PAYMENT_TIMEOUT_SECONDS=5
PAYMENT_CONNECT_TIMEOUT_SECONDS=2$response = Http::connectTimeout(config('services.payment.connect_timeout'))
->timeout(config('services.payment.timeout'))
->post(config('services.payment.url'), $payload);Now you can change the timeout in production without deploying new code.
Step 2: Log how long each call takes:
$startedAt = microtime(true);
try {
$response = Http::connectTimeout(2)->timeout(5)->post($url, $payload);
$error = null;
} catch (ConnectionException $e) {
$error = $e->getMessage();
throw $e;
} finally {
Log::info('payment.call', [
'latency_ms' => round((microtime(true) - $startedAt) * 1000),
'error' => $error ?? null,
]);
}Your logs will now look like this:
payment.call latency_ms=180 error=null
payment.call latency_ms=210 error=null
payment.call latency_ms=5001 error="Operation timed out after 5000 ms"With real numbers you can tune the timeout properly:
- If 99% of calls finish in under 300 ms, a 5-second timeout is generous and safe.
- If many calls take around 4.8 seconds, your timeout is too tight, or the provider has a problem you should report.
| Tip | What it does |
|---|---|
| 1. Connection timeout plus request timeout | Stops your app from waiting forever |
| 2. Turn timeouts into clear 503 errors | Fails fast and frees workers |
| 3. Configurable timeouts plus latency logs | Lets you tune with real data |
One last point: a timeout doesn’t fix the slow service, it protects your app from it. The next steps are retries with backoff for temporary failures and idempotency keys so a retry never charges a customer twice. Those are topics for another article.
Subscribe and don’t miss insights to help you build resilient systems to get amazing jobs… subscribe here: build-resilient-software-get-amazing-jobs
Let’s build software that keeps working when things go wrong.