Performance
Making a growing Laravel application fast again
Reports and background processes made faster by measuring first, then redistributing storage, queries and queue work.
The situation
A multi-tenant Laravel application combines visitor sessions, leads, campaign data and conversion paths in its reports. The same PostgreSQL databases were handling daily transactions, imports and increasingly demanding analytical queries.
Where it got stuck
The slowdown was not one spectacular query. Broad selects, repeated attribution calculations and reporting work inside regular requests accumulated, making small data growth noticeable in several places at once.
The technical question
The application remained in daily use and customer data was separated per company. Every improvement needed a staged rollout, a rollback path and tests around existing report results.
My approach
I started with request timings and query plans rather than assumptions. Composite indexes were added around actual filters, selects became narrower and recalculations moved to idempotent jobs. Time-series data for dashboards received a ClickHouse path while PostgreSQL remained the transactional source.
What changed
The heaviest reporting paths no longer load the operational database in the same way. Further optimisation is also measurable: each request shows whether time disappears in PHP, PostgreSQL, ClickHouse or an external integration.
Under the hood
Decisions that made the difference
Measure before changing
Request timings, query plans and job durations show where optimisation work will actually matter.
Indexes based on use
Composite indexes follow tenant, source and date filters from real reports instead of isolated columns chosen by instinct.
Asynchronous recalculation
Attribution and imports run outside web requests and can safely be restarted.
Storage by workload
The operational truth remains relational while large aggregations receive an analytical path.