Real-time Laravel query performance monitoring. Drop it in, browse your app, and instantly see N+1 queries, slow SQL, duplicates, memory spikes, and more β all in a beautiful developer dashboard.
| Feature | Description | |
|---|---|---|
| π | N+1 Detection | Finds repeated SQL patterns caused by missing eager-loading |
| π’ | Slow Queries | Flags individual queries over your ms threshold |
| π | Duplicate SQL | Catches identical queries run multiple times per request |
| π§ | Memory Tracking | Peak memory, growth per request, configurable alert threshold |
| β | SELECT * Detection | Warns when all columns are fetched unnecessarily |
| π | Index Hints | Detects leading-wildcard LIKE '%...' that force full table scans |
| π | Severity Score | Every request scored 0β100 and labelled OK / Notice / Warning / Critical |
| π₯ | Dashboard | Dark, dev-friendly UI at /queryspy |
| π | Timeline Chart | Dual-axis Chart.js bar+line showing request time and query count |
| π | Pagination | 25 entries per page with smart controls |
| β»οΈ | Auto-refresh | 10-second polling toggle on the dashboard |
| π | Copy SQL | One-click copy button on every SQL block |
| π€ | Export | Download as JSON or CSV directly from the dashboard |
| π | Response Headers | X-QuerySpy-* headers visible in your browser's Network tab |
| π | Password Protection | Optional dashboard password via .env |
| π | Environment Guard | Skips production, CLI commands, and ignored URL patterns |
| π¨ | Artisan Commands | summary, watch, clear, export |
composer require tuser/query-spyLaravel auto-discovers the service provider β no manual registration needed.
Publish the config:
php artisan vendor:publish --tag=queryspy-configAdd any of these to your .env (all are optional):
QS_ENABLED=true
QS_SLOW_QUERY_MS=100 # Flag queries slower than this (ms)
QS_MAX_QUERIES=50 # Alert when queries/request exceed this
QS_N_PLUS_ONE=5 # Same SQL pattern N times = N+1 warning
QS_SLOW_REQUEST_SECONDS=2 # Flag requests slower than this
QS_MEMORY_THRESHOLD_MB=64 # Alert when peak memory exceeds this (MB)
QS_INJECT_HEADERS=true # Add X-QuerySpy-* headers to responses
QS_LOG_CHANNEL=stack # Laravel log channel for alerts
QS_DASHBOARD_URL=/queryspy # Dashboard URL
QS_PASSWORD= # Optional dashboard password
QS_MAX_LOG_ENTRIES=200 # Max entries kept in storageFull reference: config/queryspy.php
Visit /queryspy in your browser after installing.
- 7 stat cards β Total, Critical, Warning, Notice, Clean, Avg Duration, Total N+1
- Sidebar β avg queries, avg memory, avg severity score with progress bar
- Timeline chart β request duration (bars) + query count (line), coloured by severity
- Filters β by severity level or issue type (N+1, slow, dupes, memory)
- Search β by URL, route name, or any SQL text
- Pagination β 25 per page
- Auto-refresh β 10-second toggle
- Export CSV / JSON β one-click download
- Per-request drill-down with tabs:
- Summary β all metrics, memory, index hints, SELECT* warnings
- All Queries β time badge, full SQL with Copy button, source
file:line - N+1 β pattern, count, total time, eager-load suggestion, source
- Slow β full SQL, caller, source
- Duplicates β count, source
When QS_INJECT_HEADERS=true, every response includes:
X-QuerySpy-Queries: 14
X-QuerySpy-Time-Ms: 87.4
X-QuerySpy-Memory-MB: 12.5
X-QuerySpy-Severity: warning
X-QuerySpy-Score: 45
X-QuerySpy-N1: 2
X-QuerySpy-Slow: 1
X-QuerySpy-Duplicates: 0
Visible in your browser's Network tab β select request β Headers.
# Pretty table of recent entries
php artisan queryspy:summary
php artisan queryspy:summary --limit=50
# Live tail β prints new entries as they arrive
php artisan queryspy:watch
php artisan queryspy:watch --interval=5
# Wipe all stored logs
php artisan queryspy:clear
# Export to file
php artisan queryspy:export --format=csv
php artisan queryspy:export --format=json
php artisan queryspy:export --format=csv --output=/tmp/report.csvAll endpoints accept ?password= when QS_PASSWORD is set.
GET /queryspy/api β paginated JSON log
GET /queryspy/api?severity=critical
GET /queryspy/api?page=2&per_page=20
GET /queryspy/export/csv β CSV download
POST /queryspy/clear β clear all logs
API response shape:
{
"data": [ ... ],
"stats": { "total": 42, "critical": 3, "warning": 8, ... },
"meta": { "total": 42, "page": 1, "per_page": 50, "last_page": 1 }
}QuerySpy only runs in:
// config/queryspy.php
'environments' => ['local', 'development', 'staging'],It never runs in production by default, in CLI/Artisan commands, or for URLs matching ignore_urls patterns (/queryspy*, /_ignition*, /telescope*, /horizon*).
QS_PASSWORD=my-secretThe dashboard shows a password form. Pass ?password=my-secret to API endpoints.
The same SQL pattern (with bindings normalised to ?) runs 5+ times per request. Classic sign of missing with('relation') in Eloquent.
Any individual query taking longer than QS_SLOW_QUERY_MS (default 100ms). Includes full SQL with real binding values and the source file:line.
Identical SQL (including bindings) run more than once β usually a missing cache or a logic loop.
Peak PHP memory exceeds QS_MEMORY_THRESHOLD_MB. Also tracks memory growth across the request lifecycle.
SELECT * on wide tables fetches unnecessary columns, wasting memory and network bandwidth.
LIKE '%value' (leading wildcard) can't use a B-tree index and results in a full table scan. Consider a full-text index or a dedicated search engine.
# Config
php artisan vendor:publish --tag=queryspy-config
# Blade views (to customise the dashboard)
php artisan vendor:publish --tag=queryspy-viewsSee CHANGELOG.md.
MIT Β© QuerySpy