[ GUIDE ]

Track outgoing HTTP requests in Laravel

Why slow external APIs are the sneakiest source of Laravel latency, and how to surface them with Http events, Guzzle middleware, or APM instrumentation.

QUICK ANSWER

How do I track outgoing HTTP requests in Laravel?

Laravel's Http facade fires RequestSending and ResponseReceived events you can listen to — log URL, method, status, and duration for each outbound call. For aggregation and per-host p95 trending, install an APM that records outgoing HTTP automatically. NightOwl's outgoing-requests dashboard groups by host with count, p95, and error rate per destination, tying every call back to the originating request.

Updated · 2026-04-13

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 out
  • ResponseReceived — after a response arrives
  • ConnectionFailed — when the connection couldn't be established

app/Providers/EventServiceProvider.php

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:

php
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:

php
// 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

  1. 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.
  2. Cache responses. Use Laravel's Cache facade with short TTLs for read-heavy external data (pricing tables, currency rates, etc.).
  3. 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.
  4. Parallel calls. Http::pool() fires multiple requests concurrently — turns three sequential 300ms calls into one 300ms window.
  5. 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.

bash
composer require nightowl/agent
php artisan nightowl:install

From $5/month flat. Data in your PostgreSQL.

Frequently asked questions

How do I track outgoing HTTP requests in Laravel?

Laravel's Http facade fires RequestSending and ResponseReceived events. Listen to them in a service provider to log every outbound call with URL, method, status, and duration. For richer aggregation (grouping by host, p95 per endpoint, error rates) use an APM like NightOwl that records outgoing HTTP automatically with full trace context.

What's the difference between Laravel's Http client and Guzzle?

Laravel's Http facade (since 7.x) is a wrapper around Guzzle that adds idiomatic helpers, a fake() testing API, and events (RequestSending, ResponseReceived, ConnectionFailed). Under the hood it's still Guzzle. If your code uses Guzzle directly, you won't get those events — wrap calls in Http::send() or use Guzzle middleware for equivalent instrumentation.

How do I find which external API calls are slowing down my Laravel app?

Record every outgoing HTTP call's duration and group by host. You want to see p95 latency per downstream service: Stripe, Mailgun, your CRM webhook, etc. If your /checkout p95 is 2s and Stripe's API call p95 is 1.5s, you've found your bottleneck. NightOwl's outgoing-requests dashboard groups by host with p95 and error rate per destination.

How do I handle slow external APIs in Laravel?

Four mitigations in order of effectiveness: (1) Move the call off the request path — dispatch a job, poll for result, use webhooks. (2) Add a hard timeout — Http::timeout(5). Timeouts above your endpoint SLO are useless. (3) Use circuit-breaking (laravel-circuit-breaker package or similar) to fail fast when a downstream is degraded. (4) Cache responses with TTL when the data is read-heavy.

Should I log every outgoing HTTP request body?

No. Request bodies often contain PII, API keys, or payment data. Log metadata (URL, method, status, duration, response size) but redact or omit bodies. If you need bodies for debugging, enable body logging conditionally (on errors only, or on a sample of requests) and scrub sensitive fields before storage.

How do I know if an external API call is failing silently?

You don't, unless you check the response status. Laravel's Http client doesn't throw on 4xx/5xx by default — you have to call throw(), throwIf(), or inspect $response->failed(). Use Http::macro() to configure a global default, or listen to ResponseReceived and alert when status is >= 400. Silent failures on retry-in-background jobs are the worst offender.

Can I trace a Laravel request through all its outgoing HTTP calls?

Yes — pass a correlation ID (X-Request-Id or W3C traceparent header) on every outgoing call and record it alongside the call metadata. Downstream services that log the header can be correlated back. Laravel's nightwatch package does this automatically, and both NightOwl and Nightwatch Cloud show outgoing calls per request in the trace view.

What's a reasonable timeout for outgoing HTTP in Laravel?

Depends on where you're calling from. From a user-facing request: 2-5 seconds max — your endpoint SLO bounds it. From a background job: 10-30 seconds with retries. From a webhook receiver: whatever your inbound timeout allows, minus processing overhead. Never call external APIs without a timeout; Guzzle defaults to no timeout, which means a hung downstream can hang your PHP worker indefinitely.

PRICING

Flat pricing. No event caps. No per-seat fees.

14-day free trial, no credit card. Your PostgreSQL, your data.

HOBBY

$5 /month

1 app · 14 days lookback · all Laravel events

TEAM

$15 /month

Up to 3 connected apps · unlimited environments · all Laravel events

AGENCY

$69 /month

Unlimited apps · unlimited agent instances · same flat rate at any traffic

Related