Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Server Timing Laravel Package

beyondcode/laravel-server-timing

Add Server-Timing headers to your Laravel responses so you can see exactly where time goes—middleware, database queries, and custom measurements—right in your browser's developer tools. A simple, fluent API lets you instrument any part of a request. No external services needed.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register (5 minutes).

composer require beyondcode/laravel-server-timing

The service provider is auto-discovered; the middleware is not. That is the single most important fact about this package — nothing works until you register it yourself.

Laravel 11+ (bootstrap/app.php):

->withMiddleware(function (Middleware $middleware) {
    $middleware->prepend(\BeyondCode\ServerTiming\Middleware\ServerTimingMiddleware::class);
})

Laravel 10 and below (app/Http/Kernel.php): add the same class to $middleware (global) or a route group's $middlewareGroups.

Prefer prepend/top-of-stack over append — the outermost middleware starts its clock first, so it reports the widest, most honest total request duration.

Verify before you instrument anything. Two checks, both cheap:

curl -sI https://your-app.test/ | grep -i server-timing
// Browser DevTools console — read the raw header
performance.getEntriesByType('navigation')[0].serverTiming

In DevTools the visual home is Network → click the request → Timing → "Server Timing" (Chrome and Firefox; Safari support has historically lagged — don't debug against Safari).

First use case. Find the endpoint that "feels slow," open it in DevTools, and confirm you see a single total metric. That's the whole install validated. Now wrap the suspected culprit:

use BeyondCode\ServerTiming\Facades\ServerTiming;

ServerTiming::start('report_query');
$rows = Report::where('tenant_id', $tenant)->with('lines')->get();
ServerTiming::stop('report_query');

Reload, and the header now reads something like total;dur=812.4, report_query;dur=740.1. You have converted "the report page is slow" into "740ms of the 812ms is that query." That's the entire value proposition of the package, and it's available in one request/response cycle.

Where to look first in the repo: the two files that constitute the whole package — src/Middleware/ServerTimingMiddleware.php and src/ServerTiming.php (the facade's underlying class). Read them once. They take ten minutes and eliminate all guesswork about ordering and state. Optionally publish the config if you want a package-level on/off switch rather than relying purely on middleware registration.

Implementation Patterns

Gate registration by environment, not by discipline. The highest-leverage pattern is making the middleware conditional at the container level so no one has to remember:

// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    if (! app()->isProduction()) {
        $middleware->prepend(ServerTimingMiddleware::class);
    }
})

For staging/pre-prod where you do want it in a production-like environment, gate behind an auth check or a signed cookie rather than trusting obscurity — the headers are readable by any client that gets the response.

Nest spans deliberately; don't instrument everything. start()/stop() are keyed by label, so keep a small, stable, low-cardinality vocabulary. A workable default set:

Label Where Why
db around heavy query blocks usually the biggest win
cache cache miss path proves a caching change landed
view around view()->render() template cost is invisible otherwise
http:external outbound HTTP calls third-party latency is not your code
domain:<name> the one or two hot paths the actual sprint ticket

Do not emit a span per query. You'll blow past header-size limits and nobody reads a 40-metric waterfall.

Wrap in middleware for cross-cutting spans. For anything that applies to many routes, write a tiny middleware and register it after the timing middleware:

class TimedCacheLookup
{
    public function handle($request, Closure $next)
    {
        ServerTiming::start('cache');
        $response = $next($request);
        ServerTiming::stop('cache');
        return $response;
    }
}

Because start()/stop() are keyed by label, this composes cleanly across services and controllers without threading a timer object through your call stack.

Use addMeasure() when you already have a duration. If a downstream client, job, or repository method returns its own elapsed time, don't wrap it — report it:

ServerTiming::addMeasure('search', $result->tookMs, 'elastic');
// => search;dur=42.1;desc="elastic"

This is the right tool for wrapping third-party SDKs whose internals you can't bracket, and for re-emitting timings from a sub-request.

Assert it in tests. A regression here is silent, so make it loud:

$response = $this->get('/reports');
$this->assertTrue($response->headers->has('Server-Timing'));

Better: assert the specific label exists on the endpoint you just optimized. This catches the "someone reordered the middleware" failure before it reaches staging.

Before/after as the workflow. The package's real day-to-day job is gating optimizations on evidence. Measure on a fixed dataset, ship the cache index / queue offload / eager load, measure again on the same dataset. Screenshot the DevTools Timing panel into the PR description. That's a reviewable, falsifiable artifact that costs nothing to produce.

Gotchas and Tips

The silent no-op is the primary failure mode. Middleware not registered, registered too late in the stack, or registered on the wrong route group produces no header and no error. During debugging this reads as "no timing problem" rather than "instrumentation missing." Always run the curl -sI | grep -i server-timing check first, before you start reasoning about performance.

stop() without start() (or vice versa). An unmatched call silently produces a missing or nonsense metric depending on the package version's internal state. Keep start/stop pairs within a single method body where possible; if you need try/finally, use it. An exception thrown between start and stop is the classic way to lose the label.

Streamed and file responses. StreamedResponse, BinaryFileResponse, and SSE responses may not carry the header, because timing is added to the response object before the body is transmitted — and by the time you'd want to add the metric, the response may already be flushing. If an endpoint is a download or a stream, don't expect Server-Timing to work there; instrument the controller logic and log it instead.

Octane and long-running workers. Anything stateful is a liability across requests in an Octane/Swoole/RoadRunner worker. Verify per-request state is actually reset in your environment; if you see metrics bleeding from one request into the next, that's worker state, not the browser. Test on Octane specifically before standardizing — a service on Octane deserves its own validation pass.

Header size and proxies. Every metric adds bytes. Liberal instrumentation plus a desc on each measure can push you toward header limits, and some proxies/Nginx configurations will truncate or drop large headers. Cap the milestone set, skip descriptions on high-traffic endpoints, and check client_header_buffer_size if headers mysteriously disappear in staging but not locally.

Caches and CDNs strip it. Nginx proxy_hide_header, Cloudflare, and any response-transforming layer will happily remove custom headers. If a header appears in curl on the origin but not in the browser, you're looking at a proxy, not a bug.

Information disclosure is real. Query counts, route/controller class names, and custom milestone labels leak internal architecture to anyone who reads them. Never enable this in production without a gate. This is the one item that needs a security review if the surface is multi-tenant or compliance-sensitive.

Server-Timing is per-request and browser-only. CLI commands, queue workers, scheduled jobs, native mobile clients, and third-party API consumers will never see these headers. Don't let it become the team's mental model of "we have observability" — it's a developer-facing triage convenience. It complements APM; it doesn't replace it, and it has no alerting, aggregation, sampling, or retention.

Naming discipline prevents a mess. Agree on a convention up front (db.query, cache.get, http.stripe) with bounded cardinality. Labels with variable content (query_42, user IDs, URLs) will bloat headers and make across-request comparison meaningless.

Plan for the Laravel-major bump. 1.7.0 exists solely for Laravel 13 support (PR #42) — no features, no API changes. That's the cadence: one compatibility release per framework major. Put this package on the framework-upgrade checklist, and pin the version that matches your target. The failure mode after an unpinned upgrade is exactly the silent no-op above, or a hard throw in the middleware.

Debugging order of operations. (1) Does the header exist at all via curl -I? (2) Is the middleware registered and does its position make sense? (3) Does the label appear in the header at all? (4) Does the DevTools Timing panel show it (browser-side parsing)? (5) Is a proxy stripping it? Answering in that order avoids ninety percent of wasted time.

Rollback is trivial — say so. Remove the middleware line, composer remove. No migrations, no schema, no backfill, no user-visible change. That low blast radius is precisely why this is safe to pilot on one service before standardizing the registration and the environment gate in your application skeleton.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource