Layer 1 — Http facade events
Laravel's Http client fires three events you can listen to in a service provider:
RequestSending— before the request goes outResponseReceived— after a response arrivesConnectionFailed— when the connection couldn't be established
app/Providers/EventServiceProvider.php
use Illuminate\Http\Client\Events\RequestSending;
use Illuminate\Http\Client\Events\ResponseReceived;
use Illuminate\Http\Client\Events\ConnectionFailed;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
public function boot(): void
{
Event::listen(function (ResponseReceived $event) {
Log::info('Outgoing HTTP', [
'url' => $event->request->url(),
'method' => $event->request->method(),
'status' => $event->response->status(),
'duration_ms' => $event->response->transferStats?->getTransferTime() * 1000,
]);
});
Event::listen(function (ConnectionFailed $event) {
Log::warning('HTTP connection failed', [
'url' => $event->request->url(),
'method' => $event->request->method(),
]);
});
}Good for audit trail. Missing: per-host aggregation. A single log line doesn't tell you Stripe is degraded — you need counts and percentiles over time.
Layer 2 — Guzzle middleware
If you're on Guzzle directly (not Http::), use middleware:
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
$stack = HandlerStack::create();
$stack->push(Middleware::tap(
function (RequestInterface $request) {
$GLOBALS['http_start'] = microtime(true);
},
function (RequestInterface $request, $options, $response) {
$duration = (microtime(true) - $GLOBALS['http_start']) * 1000;
Log::info('Outgoing', [
'host' => $request->getUri()->getHost(),
'path' => $request->getUri()->getPath(),
'status' => $response->getStatusCode(),
'duration_ms' => round($duration, 2),
]);
}
));
$client = new Client(['handler' => $stack]);Layer 3 — Per-host aggregation
The goal isn't logging every call — it's seeing which downstream services are hurting you. Aggregate by host:
- Request count per host (which services do we depend on most?)
- p95 duration per host (which service is slow?)
- Error rate per host (which service is failing?)
- Failure-to-timeout ratio (is this connection issues or real errors?)
Cheap to do with a outgoing_requests table and a nightly rollup job. Expensive to maintain — the APM path is usually a better tradeoff.
Set timeouts on every outgoing call
Guzzle defaults to no timeout. A hung downstream will hang your PHP worker indefinitely. Always set a timeout:
// Set per-call (recommended for user-facing requests)
Http::timeout(5)
->connectTimeout(2)
->get('https://api.stripe.com/v1/charges');
// Set globally via a macro or service provider
Http::macro('external', fn () => Http::timeout(5)->connectTimeout(2));
// Always use ->throw() when you need to fail loudly
Http::timeout(5)->get($url)->throw();Patterns that reduce outgoing HTTP pain
- Move to async jobs. If you can call an external API in a queued job instead of the request, do it. Users don't wait on webhooks.
- Cache responses. Use Laravel's Cache facade with short TTLs for read-heavy external data (pricing tables, currency rates, etc.).
- Circuit break. When a downstream fails repeatedly, fail fast instead of waiting for every timeout. Libraries like ganyicz/laravel-circuit-breaker or a simple cache-based counter work.
- Parallel calls. Http::pool() fires multiple requests concurrently — turns three sequential 300ms calls into one 300ms window.
- Correlation IDs. Pass X-Request-Id on every outgoing call so downstream logs can be correlated back to the originating request.
THE EASY WAY
NightOwl aggregates every outgoing HTTP call by host
The outgoing-requests dashboard groups every outbound HTTP call by destination host with count, p95 duration, and error rate. Click into a host to see which requests called it and what the responses looked like. Tied back to the Laravel request that fired each call.
composer require nightowl/agent
php artisan nightowl:installFrom $5/month flat. Data in your PostgreSQL.