Legacy rebuild: 3,982 database queries down to 52
An association management dashboard took 6.8 seconds to render and timed out on accounts with 800+ members. Profiling queries with Debugbar revealed 3,982 database executions on a single page load. Through nested eager loading, in-database query aggregation with withSum, and request memoization, query count dropped to 52, load time plummeted to 410 ms, and memory dropped by 79% — without caching layers or server upgrades.
The client operated an association and membership management SaaS platform. Their core operational dashboard listed members with their membership status, organizational groups, invoiced dues, and payment records.
As customer accounts grew past 800 members, the page took 6.8 seconds to load and repeatedly timed out for their largest customers, generating critical support tickets and threatening churn during renewal periods.
Beneath seemingly idiomatic Eloquent code lay a 3-level nested N+1 cascade: Blade loops iterated member → memberships → invoices → payments, generating over 3,100 round-trips for 800 rows. In addition, an Eloquent accessor calculated open invoice balances with a per-row sum query, and an unmemoized settings lookup fired 760 identical queries. In total, 3,982 database queries were executed on a single page render, consuming 289 MB of peak memory.
Rather than masking database dysfunction with Redis caching or paying for larger server instances, I ran a systematic query investigation:
1. Baseline Measurement: Seeded production-volume data locally and tracked exact query count, wall-clock time, and memory via Laravel Debugbar (3,982 queries, 6.8 seconds, 289 MB peak memory).
2. Query Shape Analysis: Grouped logged queries by SQL shape using DB::getQueryLog(). Just 5 query shapes accounted for 3,930 of the 3,982 queries.
3. Nested Eager Loading: Replaced lazy-loaded relationship loops with nested eager loading ($query->with(['memberships.invoices.payments', 'groups:id,name'])).
4. In-Database Aggregations: Replaced the PHP model accessor query with Eloquent's withSum('invoices as open_balance', 'amount_due'), offloading calculations to MySQL.
5. Memoized Configuration: Wrapped repeated global setting lookups with Laravel's once() memoization, collapsing 760 queries into 1.
6. Permanent Regression Prevention: Enabled Model::preventLazyLoading(! app()->isProduction()) and added strict query-budget assertions into PHPUnit test suites to prevent future N+1 regressions in CI.
The dashboard rendered in 410 ms (down from 6.8 seconds and timeouts), query count dropped by 98.7% (from 3,982 down to 52 queries), and peak memory dropped from 289 MB to 61 MB. The largest association accounts loaded in under a second with zero timeouts, server CPU utilization decreased substantially across the entire cluster, and no additional caching or infrastructure costs were incurred.
Something holding you back?
New build, rebuild, or adding AI to what you already have — I'll take a look and give you an honest perspective.