Laravel at scale: the boring parts that matter
Laravel gets a product to launch fast. Keeping it calm under real load is a different discipline — queues, caching, and observability done right, long before a 3am incident forces the issue.
Laravel is exceptional at getting a product from zero to a working, demo-able system fast. Eloquent, Blade, the ecosystem of first-party packages — it is a framework tuned for velocity. Keeping that same application calm once real users, real traffic, and real data volume show up is a different discipline entirely, and it is one that gets skipped far too often because nothing forces the issue until the first serious incident. These are the boring, unglamorous practices that separate a Laravel app that survives its first viral moment from one that falls over.
Queues: anything external belongs off the request cycle
Any request that touches an external service — sending an email, calling an LLM API, processing a payment webhook, generating a PDF — belongs on a queue, not inline in the HTTP request-response cycle. This sounds obvious stated plainly, and yet it is the single most common performance issue we find in Laravel codebases that were built under deadline pressure: a controller action that calls a third-party API synchronously, works fine in testing against a fast mock, and then times out in production the first time that third party has a slow day.
Horizon plus a dedicated worker fleet, sized and monitored separately from the web servers, is table stakes for anything beyond a small internal tool. Just as important as adopting queues is instrumenting them: track queue depth, job failure rate, and time-to-process per queue name, and alert on queue depth climbing rather than discovering a backlog when a user complains their email never arrived. A queue nobody watches is just a slower, harder-to-debug version of a synchronous call.
Caching: cache the query, not the response
The instinct to reach for response caching — cache the entire rendered page or API response — feels like the fastest win, and it is, right up until it hides a real underlying problem instead of solving it. A cached response masks a slow query or an N+1 problem until the cache expires or gets busted, at which point the same slowness returns, usually at the worst possible traffic moment, and now with less debugging context because the team forgot the underlying query was ever slow.
Caching at the query and computation layer — the result of an expensive aggregation, a computed permission set, a report that only needs to be accurate to within a few minutes — is more work to set up correctly, with proper cache keys and invalidation, but it fails safely. When the cache misses, the underlying code path still needs to be fast enough to serve a real request, which keeps the team honest about actual query performance instead of papering over it.
Observability: answering 'is the site OK right now' in one glance
Sentry or an equivalent for error tracking, OpenTelemetry or a comparable tracing setup for request-level visibility, and — critically — a single dashboard that answers one question in under five seconds: is the site okay right now. Without that dashboard, incidents become investigations rather than responses. Someone gets paged, and the first twenty minutes of the incident are spent figuring out what is even wrong, rather than fixing it, because the relevant data lives in six different tools nobody checks together.
The dashboard does not need to be sophisticated. Error rate, p95 response time, queue depth, and database connection saturation, on one screen, updated in near real time, catches the large majority of incidents before a customer notices. Building that dashboard is unglamorous work that never shows up in a feature demo, which is exactly why it gets deprioritized — right up until the first 3am page that could have been a five-minute fix instead of a two-hour scramble.
Database discipline: the part teams skip until it hurts
Every scaling story we have worked through eventually comes back to the database: missing indexes on columns used in WHERE clauses and joins, N+1 queries hiding behind Eloquent relationships that look innocent in code review, and connection pool exhaustion once traffic outpaces a default configuration nobody revisited since the project's first week.
None of this is exotic. It is running EXPLAIN on slow queries, eager-loading relationships instead of lazy-loading them in a loop, and a connection pool sized for the traffic the app actually receives rather than the traffic it had at launch. The teams that stay calm at scale are not the ones with the most exotic infrastructure — they are the ones that treat these boring fundamentals as part of the definition of 'done' for a feature, not as cleanup work for later.