Layer 1 — Web server access logs
Nginx and Apache can record request duration. Free, always-on, zero Laravel code.
nginx.conf
log_format timing '$remote_addr - [$time_iso8601] '
'"$request" $status $body_bytes_sent '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log timing;
Sort the log by $request_time to find slow requests. What you're missing: controller name, user context, query breakdown, per-route aggregation. Fine for a quick baseline, useless for root cause.
Layer 2 — Middleware timing
A Laravel middleware records start/end time and the resolved route. You get controller-aware timing without a full APM.
app/Http/Middleware/MeasureRequestTime.php
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class MeasureRequestTime
{
public function handle(Request $request, Closure $next)
{
$start = microtime(true);
$response = $next($request);
$duration = (microtime(true) - $start) * 1000;
if ($duration > 500) {
Log::warning('Slow request', [
'route' => $request->route()?->uri(),
'controller' => $request->route()?->getActionName(),
'method' => $request->method(),
'duration_ms' => round($duration, 2),
'status' => $response->getStatusCode(),
'user_id' => auth()->id(),
]);
}
return $response;
}
}
Register globally in bootstrap/app.php. Good for logging outliers. Missing: aggregation. A single slow request doesn't tell you much — you want to know which route pattern is consistently slow at p95.
Layer 3 — Route aggregation with p95
The real unlock is aggregating by normalized route pattern. One record per request, grouped by route?->uri(), with p95 and p99 percentiles computed over a time window.
Metrics to surface per route:
- Request count
- Error rate (5xx %)
- p50 / p95 / p99 duration
- Throughput (requests/min)
- Average DB time / external HTTP time per request (requires instrumentation layer)
Layer 4 — Per-request trace correlation
Once you've identified /orders/{id} is slow at p95, the next question is why. You need per-request traces that break down the slow request into its component spans: DB queries, cache calls, external HTTP, view rendering.
The Laravel Nightwatch package records every span per request with a shared trace ID. NightOwl and Nightwatch Cloud both consume this data — you drill from slow route to slow request to slow query in three clicks.
Fix priorities — what to tackle first
- High-traffic routes with high p95 — biggest total pain. Often N+1s or missing indexes.
- Low-traffic routes with catastrophic tail — /api/export, /admin/report. Users don't hit them often, but when they do they hurt.
- Routes trending slower over time — data growth outpacing indexes. Profile the plan.
- Routes blocked on external HTTP — move to async/queued work. See the outgoing HTTP guide.
- Routes with high error rate AND high p95 — failing slowly, worst possible combination.
THE EASY WAY
NightOwl aggregates every request by route with p95 and full trace drilldown
The requests dashboard groups by route pattern with count, p95 / p99, and error rate. Click a route to see its slowest requests. Click a request to see every DB query, cache call, and HTTP span. All built on the official laravel/nightwatch instrumentation — zero impact on request path.
composer require nightowl/agent
php artisan nightowl:installFrom $5/month flat. Data in your PostgreSQL.