Skip to main content

ยท Performance

Scaling Laravel for Millions: A Performance Deep Dive

A technical post-mortem on scaling Laravel to handle enterprise-grade traffic and millisecond-precision response times.

Scaling Laravel for Millions: A Performance Deep Dive

Most Laravel applications that struggle under load are not slow because of Laravel. They are slow because of a handful of decisions that were reasonable at a hundred users and are ruinous at a hundred thousand. Here is where the time actually goes, in the order we usually find it.

The N+1 query is still the biggest single cost

Eloquent makes it very easy to write a loop that issues one query per row without ever looking like it does. A listing page that renders fifty records, each touching a relationship in the view, quietly runs fifty one queries. At low traffic nobody notices. At scale it is the difference between a page that responds in 40 milliseconds and one that takes two seconds.

The fix is eager loading, and the way to find them is not reading code. Enable query logging in a staging environment and count queries per request. Anything rendering a list and issuing more than a handful of queries is worth opening. Laravel Debugbar or Telescope will show you this in an afternoon.

The subtler version is eager loading too much. Loading a relationship the view never touches costs a query and the memory to hydrate every model. Load what the page uses, not what it might use.

Hydrating models you are about to throw away

Eloquent builds a full object for every row, with attribute casting, relations and change tracking. That is exactly what you want for a record you are about to modify, and pure overhead for a report you are about to count.

For read-only aggregate work, the query builder returns plain objects and skips all of it. For large exports, chunking keeps memory flat instead of loading a hundred thousand models into an array and hitting the memory limit. These are not micro-optimisations. On a big result set the difference is an order of magnitude.

Cache the expensive thing, not the whole page

Full page caching is the blunt instrument, and it stops being usable the moment the page is personalised. What holds up better is caching the specific expensive fragment: the query that aggregates, the API call to a slow partner, the computed navigation tree.

Two rules make cache safe rather than dangerous. Give every key a deterministic name that includes the inputs, so two different results cannot collide. And decide expiry deliberately: a cache with no expiry is a bug that shows up weeks later as data nobody can explain.

Redis rather than the file driver, once you are on more than one server. File cache on a load balanced pair means two caches that disagree with each other.

Move work out of the request

The user is waiting. Anything that does not affect what they see next does not belong in the request: sending the email, generating the PDF, calling the analytics endpoint, resizing the upload.

Queues are the standard answer and the standard mistake is running them without supervision. A queue worker that has silently died looks exactly like a system where nothing needs doing, right up until someone asks why no email has gone out since Tuesday. Run workers under a supervisor that restarts them, and monitor queue depth as a first class metric.

The database is usually where the ceiling is

Application servers scale sideways easily. The database does not, which is why it becomes the constraint first.

  • Index what you filter and sort on, and confirm with EXPLAIN rather than assuming. An index that is not used is disk and write cost for nothing.
  • Read replicas take reporting load off the primary. Laravel supports a read and write connection split natively, and the trap is replication lag: a record written and read back immediately may not be there yet.
  • Connection limits arrive earlier than people expect. Every PHP worker holds a connection, so the ceiling is workers times servers, not requests.

Measure before you change anything

Every item above is worth doing when it is your bottleneck and worth nothing when it is not. Application performance monitoring on a real environment will tell you which in an hour. Optimising the wrong layer is how a team spends a month on caching when the answer was one missing index.

If you want this looked at on a system that is already live, that is what our web application work covers.

More reading

Tell us what you are building.

No sales sequence. One person reads this and replies. Rather give more detail?