The Problem It Solves
When a Laravel request is slow, the time is going into one of six places, and they need completely different fixes:
| Stage | If it is slow, the cause is usually |
|---|---|
| Bootstrap | Too many service providers, uncached config, autoloader not optimised |
| Middleware | A global middleware doing a query or an HTTP call on every request |
| Routing | Uncached routes, or an expensive route model binding |
| Controller | N+1 queries, a slow external API, unbounded collections |
| Response | Heavy Blade rendering, serialising a large payload |
| Terminate | Post-response work that is not actually deferred |
Without a timeline you guess, and people almost always guess “controller” because that is the code they wrote. A surprising amount of the time it is a global middleware nobody remembers adding.
Laravel Time Machine is a free, MIT-licensed profiler that records a full lifecycle timeline per request, profiles every SQL query with its bindings and duration, and renders it as a visual dashboard. It is the most popular of my packages at 68 stars and 2,900+ downloads.
Installing It
composer require jaydeep/laravel-time-machineThat is enough to start. It follows APP_DEBUG by default, so it is on locally and off in production without you configuring anything. Publish the config when you want to change that:
php artisan vendor:publish --tag=time-machine-config| Requirement | Supported |
|---|---|
| PHP | 7.4 and 8.x |
| Laravel | 8, 9, 10, 11, 12, 13 |
| License | MIT |
The Dashboard
Visit /time-machine. You get a list of recent requests with method, URL, status code, total duration, memory and query count. Filter and search by any of those, then click a request to open its timeline.
The filtering is what makes it useful on a real application. “Show me POST requests to /api/orders that took over 500 ms” narrows a thousand captured requests down to the four that matter.
Reading the Timeline
Each request renders as a Gantt-style chart of its stages:
Request: POST /api/orders Total: 1,840ms
Bootstrap ██ 42ms
Middleware ████ 95ms
Routing █ 11ms
Controller ████████████████████████████████ 1,610ms
├─ SQL (34 queries) 1,204ms
├─ external-api span 310ms
└─ (php) 96ms
Response ███ 68ms
Terminate █ 14msThat output answers the question in one line: 34 queries taking 1.2 seconds inside the controller. Now you know to go looking for an N+1, and you know the external API is a secondary concern rather than the cause.
Look at the query count before the query time. Thirty-four queries on an endpoint that should need three is a structural problem — and fixing it usually removes more time than optimising any individual query would.
SQL Query Profiling
Every query is captured with its SQL, its bindings and its duration, and anything over the slow-query threshold is highlighted. Bindings matter more than people expect — a query that is fast for one value and slow for another is invisible without them.
// config/time-machine.php
'collectors' => [
'queries' => true,
],
'thresholds' => [
'slow_request' => 500, // ms — highlight the request
'slow_query' => 50, // ms — highlight the query
],Duplicate queries are the tell for N+1. If you see the same statement with only the bound ID changing, thirty times in a row, that is an eager load you did not write — and there is a whole article on fixing it.
Custom Spans and Marks
The built-in stages cover the framework. For your own code, the facade lets you instrument anything:
use Jaydeep\LaravelTimeMachine\Facades\TimeMachine;
// A point in time
TimeMachine::mark('cache primed');
// Measure a callable and get its return value back
$report = TimeMachine::measure('generate-report', function () {
return Report::build();
});
// Manual start/end when the work is not in one block
TimeMachine::startSpan('external-api');
$response = Http::timeout(10)->get($endpoint);
TimeMachine::endSpan('external-api');measure() is the one to reach for — it wraps a closure, times it, and returns whatever the closure returned, so adding instrumentation does not restructure your code:
public function store(StoreOrderRequest $request)
{
$order = TimeMachine::measure('create-order', fn () =>
$this->orders->create($request->validated())
);
TimeMachine::measure('charge-card', fn () =>
$this->payments->charge($order)
);
TimeMachine::measure('render-invoice', fn () =>
$this->invoices->render($order)
);
return new OrderResource($order);
}Three lines of instrumentation and the timeline now attributes the controller’s 1.6 seconds to a specific step instead of leaving it as one opaque bar.
Configuration
| Key | Default | Purpose |
|---|---|---|
enabled | follows APP_DEBUG | Master on/off switch |
dashboard.path | time-machine | Dashboard URI prefix |
dashboard.middleware | ['web'] | Dashboard access guards |
dashboard.per_page | 15 | Requests per page |
storage.max_records | 100 | Profiles retained |
storage.path | storage/time-machine | Where profiles are written |
collectors.queries | true | Capture DB queries |
ignore_paths | assets, telescope | Never profiled |
thresholds.slow_request | 500 ms | Request highlighting |
thresholds.slow_query | 50 ms | Query highlighting |
storage.max_records defaults to 100 and keeps a rolling window, so the profile directory cannot grow without bound while you are debugging.
Using It Safely
Profiling data is sensitive: query bindings contain real values, URLs contain real IDs, and the dashboard is a window into your traffic.
// config/time-machine.php
'enabled' => env('TIME_MACHINE_ENABLED', env('APP_DEBUG', false)),
'dashboard' => [
'middleware' => ['web', 'auth', 'can:viewTimeMachine'],
],
'ignore_paths' => [
'telescope*',
'horizon*',
'_debugbar*',
'build/*',
'*.css',
'*.js',
],The package is designed for zero overhead when disabled, and the safe default is to leave it that way in production. If you must profile a live issue, gate it behind a flag you can flip for a short window, restrict the dashboard to an admin gate, and turn it off again when you are done.
vs Telescope and Debugbar
| Time Machine | Telescope | Debugbar | |
|---|---|---|---|
| Focus | Lifecycle timing | Everything — jobs, mail, cache, events | In-page debug info |
| Timeline view | Gantt per stage | No | No |
| Custom spans | Yes | No | Limited |
| Storage | Files, rolling window | Database | Session / files |
| Setup | One composer require | Migration + config | One composer require |
| Overhead | None when disabled | Meaningful — writes to DB | Low |
They are complementary rather than competing. Telescope is the better general-purpose inspector — if you want to see queued jobs, sent mail and cache hits, use Telescope. Time Machine answers one question specifically, which is where did the milliseconds go, and it is the only one of the three that draws you a timeline.
Links
- Packagist: jaydeep/laravel-time-machine
- GitHub: JaydeepGadhiya/laravel-timemachine
- More of my work: jaydeepgadhiya.netlify.app