3 min read

The Most Common Laravel Mistakes I Still See in Production Apps

These aren’t beginner mistakes. They’re the Laravel patterns I still see in real production apps—and what to do instead before they cost you performance, time, or sleep.
The Most Common Laravel Mistakes I Still See in Production Apps

Laravel is elegant, expressive, and powerful. It’s also forgiving in a way that can quietly let bad habits survive all the way into production.

What follows is not a list of beginner mistakes. These are patterns I still see in real, shipping applications—apps with users, revenue, and pager duty. Most of them come from good intentions. All of them eventually cost time, money, or sanity.

Let’s walk through the usual suspects.


1. Business Logic Living in Controllers

Controllers should be traffic cops, not decision-makers.

Yet it’s common to see controllers doing things like:

  • Calculating totals
  • Applying discounts
  • Coordinating multiple models
  • Calling external services directly

This makes controllers:

  • Hard to test
  • Hard to reuse
  • Easy to break during “quick” changes

Better pattern:
Move business logic into:

  • Service classes
  • Action classes
  • Domain objects

Controllers should mostly validate input, call a service, and return a response. If a controller method feels “long,” that’s usually your cue.


2. Silent N+1 Queries

Laravel makes relationships feel effortless. That’s both a gift and a trap.

$users = User::all();

foreach ($users as $user) {
    echo $user->posts->count();
}

This works. It just also runs one query per user.

In production, this shows up as:

  • Random slowness
  • Spiky database load
  • “It’s fast locally” confusion

Better pattern:
Always think in sets.

$users = User::with('posts')->get();

If you’re looping and touching relationships, eager loading should be a reflex.


3. Treating .env as Runtime Configuration

.env is for boot-time configuration, not application logic.

Mistakes I still see:

  • Reading env() directly outside config files
  • Changing .env values and wondering why nothing happened
  • Conditional logic based on env() values

This breaks when:

  • Config caching is enabled
  • You deploy to multiple environments
  • You scale horizontally

Better pattern:

  • Read from config()
  • Cache configuration in production
  • Treat .env as write-once-per-deploy

If php artisan config:cache scares you, that’s a smell worth investigating.


4. No Caching Strategy (or Accidental Over-Caching)

Caching isn’t just Redis “somewhere.”

Common issues:

  • No caching at all
  • Caching everything without invalidation
  • Mixing request cache, model cache, and view cache randomly

This leads to:

  • Stale data bugs
  • Cache clears as a “fix”
  • Fear of touching cache-related code

Better pattern:
Be intentional:

  • Cache expensive queries
  • Cache derived data
  • Define clear expiration or invalidation rules

Caching should reduce work—not introduce mystery.


5. Jobs That Should Be Queued (But Aren’t)

If a request:

  • Sends emails
  • Calls third-party APIs
  • Processes files
  • Generates reports

…and it happens inline, you’re borrowing time from your users.

Symptoms:

  • Slow requests
  • Timeouts under load
  • “It only happens sometimes”

Better pattern:
Default to queues.

  • Queue emails
  • Queue notifications
  • Queue heavy logic

Then monitor them. Laravel Horizon exists for a reason.


6. No Observability Until Something Breaks

Many apps go to production with:

  • No logging strategy
  • No error tracking
  • No performance visibility

Then the first question during an incident is:

“Can we reproduce this?”

Better pattern:
Before production:

  • Centralize logs
  • Track exceptions
  • Measure slow queries and slow requests

You don’t need perfection. You need signals.


7. Overusing Magic (Because It’s Convenient)

Laravel’s magic is powerful:

  • Model events
  • Global scopes
  • Accessors everywhere
  • Implicit behavior

Used sparingly, it’s beautiful. Overused, it becomes invisible complexity.

Symptoms:

  • “Where is this value coming from?”
  • “Why does this query behave differently?”
  • Fear of refactoring

Better pattern:
Prefer explicit behavior in critical paths. Magic should surprise less, not more.


8. Skipping Tests Because “It’s Just CRUD”

CRUD is where bugs hide.

Common rationales:

  • “It’s simple”
  • “We’ll add tests later”
  • “Manual testing is faster”

Until:

  • Validation rules change
  • A relationship changes
  • A refactor breaks assumptions

Better pattern:
At minimum:

  • Feature tests for critical flows
  • Tests around business rules
  • Tests that lock in expectations

Tests aren’t for confidence. They’re for change.


9. Not Using Database Constraints

Laravel validation is great. It is not a substitute for the database.

Common omissions:

  • Missing foreign keys
  • No unique constraints
  • Nullable columns that shouldn’t be

This leads to:

  • Orphaned data
  • Subtle bugs
  • Cleanup scripts nobody wants to write

Better pattern:
Let the database protect itself.

  • Use foreign keys
  • Enforce uniqueness
  • Be explicit with nullability

Laravel works with the database, not instead of it.


10. Growing Without Architecture Re-evaluation

The app that starts as:

“Just an admin panel”

Often becomes:

  • A public API
  • A mobile backend
  • A multi-tenant system

But the architecture stays frozen in “v1 thinking.”

Better pattern:
Periodically ask:

  • Does this structure still fit?
  • Are responsibilities clear?
  • Where are the seams?

Refactoring is not failure. It’s maturity.


Final Thought

None of these mistakes mean someone is a bad developer. They usually mean the app succeeded faster than its structure.

Laravel lets you move quickly. The real skill is knowing when to slow down and clean up.

If you recognize a few of these in your own codebase, congratulations—you’re building something real.

The next step is making it durable.