fetzi/server-timing
Add Server-Timing headers to Laravel responses and expose app, database, and custom performance metrics in your browser's DevTools Performance tab. Lightweight middleware for profiling without extra tooling.
fetzi/server-timing is a tiny, framework‑agnostic helper. It has no Laravel service provider, no config file, no facade — you construct Fetzi\ServerTiming\ServerTiming, call start() / stop() (or measure()) around code you want to profile, and read getHeader() to get the Server-Timing header value.
composer require fetzi/server-timing
Minimal wiring — bind it as a request-scoped singleton and add one middleware:
// App\Providers\AppServiceProvider::register()
$this->app->scoped(\Fetzi\ServerTiming\ServerTiming::class, fn () => new \Fetzi\ServerTiming\ServerTiming());
// App\Http\Middleware\ServerTiming
public function __construct(private \Fetzi\ServerTiming\ServerTiming $timing) {}
public function handle(Request $request, Closure $next)
{
$this->timing->start('app');
$response = $next($request);
$this->timing->stop('app');
$response->headers->set('Server-Timing', $this->timing->getHeader());
return $response;
}
Register it globally (or as a group middleware) in bootstrap/app.php / Kernel.php.
Where to look first: the package README — that's basically the whole API. There's nothing to publish. The real "first use case" is: wrap one controller action, hit it in Chrome, open DevTools → Network → your request → Timing tab; you'll see your named metric. That feedback loop is faster than Telescope for quick "where is my 200 ms going" checks.
Named phases around slow work. The most useful pattern is coarse named phases rather than per-query granularity:
$timing->measure('db', fn () => $repository->loadAggregates());
$timing->measure('http', fn () => Http::get('https://api.internal/...')->json());
$timing->start('render');
return view('dashboard', $data);
Instrumenting the query layer. DB::listen fires after a query, so it can't measure durations directly. Instead wrap at the repository/service boundary, or use a query-log listener that records $query->time and aggregates into a single db metric after the response is built.
Queue / jobs. Because the binding is scoped, the container gives you a fresh instance per request and per Octane request. In a job you can wrap the handle body and log getHeader() — useful for spotting slow jobs, though the header itself only matters on HTTP responses.
Blade partials. For a specific slow view section: @php app(ServerTiming::class)->start('sidebar') @endphp ... @php app(ServerTiming::class)->stop('sidebar') @endphp. Only do this for known-slow sections; it clutters templates fast.
Combine with existing telemetry. If you already use Telescope or a metrics package, feed the same timings into both rather than duplicating start/stop calls — e.g. a small TimingService that fans out to ServerTiming and your APM.
Production vs. local. Gate the middleware by environment or a feature flag so production responses don't carry internal metric names. If you want it in production for debugging specific users, key it off a header or a config flag.
DevTools-driven workflow. Add metrics, reload the page, check the Timing tab. Good candidates: DB, Redis, external HTTP, template render, and an overall app metric. That's usually 4–6 entries — enough to isolate the slow layer.
start/stop calls.scoped() (or bind() + explicit reset() in middleware). A plain singleton() will leak metrics across requests under Octane/RoadRunner/Swoole — you'll see duplicate or accumulating entries in the header.terminate() is too late for most SAPIs; set it in handle() after $next() returns. For streamed responses, set it before the first byte is flushed.db:users, db:orders) and stop every started metric — an unstopped metric may produce a bogus/infinite duration.db, redis, tpl).desc values are visible to clients. Avoid table names, endpoint URLs, tenant IDs. In production, prefer generic names.Server-Timing if the response also carries Timing-Allow-Origin. Same-origin DevTools access works regardless.db.users, db.orders) and do the math yourself.~0 and are usually not worth a metric.$response->headers->get('Server-Timing') in feature tests; also assert it's absent when the feature flag is off, so you don't accidentally ship it.hrtime() and a header setter gives you the same result with full control.Server-Timing through by default, but any proxy that strips unknown headers (some CDN configs, WAFs) will eat it. Verify in staging via curl -I.How can I help you explore Laravel packages today?