Laravel gets criticized for not scaling, which is mostly a misdiagnosis. What actually fails at scale is a handful of default-friendly choices that were fine at ten thousand rows and become a problem at ten million. None of them are Laravel's fault; they are just easy to reach for early and expensive to unwind later.
The queue you did not mean to write
Synchronous jobs — sending the welcome email, resizing the upload, calling the third-party API — are fine until traffic makes the request thread wait on someone else's uptime. We push anything with an external network call onto a queue from week one, even before load requires it, because retrofitting queue-safety into a controller full of side effects later means re-testing every path that touches it.
ProcessUpload::dispatch($upload)
->onQueue('media')
->delay(now()->addSeconds(2));
The delay above is not decorative — it gives the transaction that created the row time to commit before the worker picks it up, which matters more than people expect once you are running multiple queue workers against a primary/replica database split.
Cache invalidation is the real feature
Caching a slow query is the easy 20%. The 80% is deciding, precisely, which cache keys go stale when a write happens — and building that into the model layer instead of scattering Cache::forget() calls through controllers. We tag caches by the entity they depend on and clear by tag in a model observer, so a developer adding a new endpoint six months later cannot forget to invalidate something they have never heard of.
- Tag cache entries by the model they read from, not by the route that wrote them.
- Invalidate in observers, not controllers — controllers change more often than data relationships do.
- Treat a cache with no invalidation path as a correctness bug, not a performance win.
N+1 in production, not staging
Eager loading gets checked in code review against staging data, where a list page has eight rows. In production it has eight thousand, and the same query that looked fine becomes eight thousand round trips to the database. We run Laravel's query log against a production-sized seed before every release that touches a list or report view — staging data with realistic row counts catches this class of bug before a client does.
What actually bites two years later
The architectural choice that costs the most later is not a technology pick — it is a fat model with business logic wired directly to Eloquent relationships, because it is the fastest way to ship a feature under deadline. It works right up until two features need slightly different versions of the same logic, and untangling it means touching every place that model is used. We push toward action classes and form requests early specifically so that "slightly different" stays a small diff, not a rewrite.
Found this useful?
Share it with someone who is planning a build.




