Why Your App Feels Slow (And How I Actually Fix It)
My performance process always starts the same way: I stop guessing and measure first. The number of times I have seen someone add Redis caching to a route that was slow because of a missing index is painful. The caching helped slightly, but the index would have solved it completely.
Step 1: Measure Before You Touch Anything
You need numbers before you touch any code. I use three tools:
- Laravel Telescope or Laravel Debugbar locally — shows every query, cache hit/miss, mail, and job triggered per request
- slow_query_log in MySQL — catches the worst offenders at the database level
- Blackfire.io for deep profiling — flame graphs, call stack analysis, memory allocation per function
Most of the time the first two are enough. Telescope will show you a route making 120 queries, and that is your problem right there.
Database Queries: The #1 Culprit
In my experience, 70% of slow app complaints trace back to the database. The most common issues:
- N+1 queries — fixed with eager loading
- Missing indexes — run EXPLAIN on slow queries and look for type: ALL
- ORDER BY without an index — sorting 2 million rows with no index on that column is catastrophic
- SELECT * on fat tables — selecting 40 columns when you need 4 wastes I/O
-- Run this on any slow query
EXPLAIN SELECT * FROM orders WHERE user_id = 1 ORDER BY created_at DESC LIMIT 10;
-- If you see type=ALL in the result, add:
ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at DESC);
PHP-FPM: Are You Starving Your Workers?
A misconfigured PHP-FPM pool is a sneaky performance killer. If your pool has too few workers, concurrent requests queue up and wait. Check and tune:
# /etc/php/8.4/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 40 # (available_RAM - OS_overhead) / avg_PHP_memory
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 500 # Restart worker after N requests — prevents memory leaks
OPcache Is Non-Negotiable
PHP parses and compiles your source files on every request without OPcache. Never run without it in production:
; /etc/php/8.4/fpm/conf.d/10-opcache.ini
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 0
opcache.validate_timestamps = 0
opcache.jit = tracing
opcache.jit_buffer_size = 128M
Application-Level Caching
// Cache expensive aggregations, not individual lookups
Cache::remember('dashboard:stats', 600, function () {
return [
'total_users' => User::count(),
'revenue_today' => Order::today()->sum('amount'),
'active_sessions'=> Session::active()->count(),
];
});
HTTP-Level Quick Wins
Enable gzip in Nginx — reduces JSON response sizes by 60-80%:
gzip on;
gzip_types application/json text/html text/css application/javascript;
gzip_min_length 1000;
gzip_comp_level 6;
The Priority Order
- Fix N+1 queries and add missing database indexes (highest impact)
- Enable and tune OPcache
- Add application-level caching for expensive queries
- Tune PHP-FPM worker count
- Enable HTTP response compression
- CDN for static assets
- Read replicas when you are genuinely I/O bound
Most apps only need steps 1 through 4. Steps 6 and 7 are for when you have genuinely exhausted the earlier ones.