📁 Volume II: Laravel Performance Tuning Kit

⚙️ Topic 3: Config Caching

The env() trap that silently kills your performance.

"After config:cache, env() returns null.
Your database connection dies. Your app crashes.
And you blame Laravel."
⚠️ CRITICAL WARNING

This is the #1 mistake that takes down production Laravel apps. Developers use env() in their code, run php artisan config:cache, and suddenly their app returns null for every config value. The app dies. They spend hours debugging.

🔴 The Problem: Reading .env on Every Request

WHAT LARAVEL DOES WITHOUT CONFIG CACHE

On every single HTTP request, Laravel:

  1. Opens and reads .env file from disk
  2. Parses every line looking for KEY=VALUE pairs
  3. Builds an array of environment variables in memory
  4. Also reads all config/*.php files (20-50 files typically)
  5. Then discards everything at the end of the request

How developers typically write config files:

// config/database.php (typical — WRONG for production)
return [
    'connections' => [
        'mysql' => [
            'host' => env('DB_HOST', '127.0.0.1'),     // ← env() call
            'port' => env('DB_PORT', '3306'),          // ← env() call
            'database' => env('DB_DATABASE', 'forge'), // ← env() call
            'username' => env('DB_USERNAME', 'forge'), // ← env() call
            'password' => env('DB_PASSWORD', ''),      // ← env() call
        ],
    ],
];

// In controllers or models (EVEN WORSE):
$host = env('DB_HOST');        // ← Reads .env file directly!
$apiKey = env('STRIPE_KEY');   // ← Another file read!
THE COST

Every env() call = file I/O operation = reading .env from disk. If you have 50 config values, that's 50 file reads per request. If you call env() inside a loop, you're dead.

🎨 What Happens Without Config Cache (Visualized)

│ │ WITHOUT CONFIG CACHE — Every request │ ═══════════════════════════════════════════════════════════════════ │ │ HTTP Request arrives │ │ │ ▼ │ ┌─────────────────────────────────────────────────────────────┐ │ │ config/app.php: env('APP_NAME') │ │ │ → open('.env') → read → return 'MyApp' │ │ │ │ │ │ config/database.php: env('DB_HOST') │ │ │ → open('.env') → read → return 'localhost' │ │ │ │ │ │ config/database.php: env('DB_PORT') │ │ │ → open('.env') → read → return '3306' │ │ │ │ │ │ config/database.php: env('DB_DATABASE') │ │ │ → open('.env') → read → return 'myapp_db' │ │ │ │ │ │ ... repeat 40 more times ... │ │ │ │ │ │ TOTAL: 50+ file opens per request │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ ▼ │ Response sent (after 40ms of wasted I/O) │ │ ═══════════════════════════════════════════════════════════════════ │ │ WITH CONFIG CACHE — One request │ ═══════════════════════════════════════════════════════════════════ │ │ HTTP Request arrives │ │ │ ▼ │ ┌─────────────────────────────────────────────────────────────┐ │ │ require('bootstrap/cache/config.php') │ │ │ │ │ │ $config = [ │ │ │ 'app' => [ │ │ │ 'name' => 'MyApp', // Already resolved │ │ │ 'env' => 'production', // No env() calls │ │ │ ], │ │ │ 'database' => [ │ │ │ 'host' => 'localhost', // Pre-resolved │ │ │ 'port' => '3306', │ │ │ 'database' => 'myapp_db', │ │ │ ], │ │ │ ]; │ │ │ │ │ │ TOTAL: 1 file open per request │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ ▼ │ Response sent (after 2ms) │

💀 The env() Trap: What Kills Production

THE DEADLY SEQUENCE

Step 1: Developer writes code like this:

// app/Http/Controllers/UserController.php
$apiKey = env('STRIPE_SECRET_KEY');  // ← PROBLEM

Step 2: Developer runs php artisan config:cache

Step 3: Laravel generates cached config... but env() outside config files is NOT evaluated

Step 4: env('STRIPE_SECRET_KEY') returns null

Step 5: Payment processing fails. App crashes. Developer confused.

Why does this happen?

│ │ How Laravel Config Cache Works: │ ═══════════════════════════════════════════════════════════════════ │ │ Step 1: php artisan config:cache │ │ │ ▼ │ Step 2: Laravel loads ALL config files (config/*.php) │ │ │ ▼ │ Step 3: Laravel executes env() calls INSIDE config files ONLY │ (reads .env and replaces env('KEY') with actual value) │ │ │ ▼ │ Step 4: Laravel saves the result to bootstrap/cache/config.php │ │ │ ▼ │ Step 5: env() calls OUTSIDE config files are NOT cached │ They will try to read .env again... │ │ │ ▼ │ Step 6: But after config:cache, Laravel UNLOADS the .env file │ to save memory. env() returns null. │

✅ The Fix: One Command + One Golden Rule

THE SOLUTION
php artisan config:cache

But this command is dangerous if you break the golden rule.

🔴 THE GOLDEN RULE OF CONFIG CACHING:
NEVER use env() outside config/*.php files.

👎 WRONG (Will crash after cache)

// config/app.php
'name' => env('APP_NAME'), // ← OK here

// app/Http/Controllers/HomeController.php
$appName = env('APP_NAME'); // ← DEATH. Will return null after cache.

// database/migrations/2024_01_01_create_users_table.php
DB::statement('CREATE DATABASE ' . env('DB_DATABASE')); // ← DEATH

👍 RIGHT (Works before and after cache)

// config/app.php
'name' => env('APP_NAME'), // ← OK here

// app/Http/Controllers/HomeController.php
$appName = config('app.name'); // ← OK. Uses cached config.

// database/migrations/2024_01_01_create_users_table.php
$database = config('database.connections.mysql.database'); // ← OK

📊 The Numbers: Before vs After

👎 WITHOUT CONFIG CACHE

.env file reads per request1 (opens entire file)
Config files parsed per request20-50
env() calls executed30-100
Time per request (config only)15-40ms

👍 WITH CONFIG CACHE

.env file reads per request0 (cached)
Config files parsed per request1 (cached array)
env() calls executed0
Time per request (config only)1-3ms

Real-world impact on a typical Laravel app:

MetricWithout CacheWith CacheImprovement
Config file I/O per request ~50 files 1 file 98% ↓
Time spent on config (per request) 25ms 2ms 92% ↓
10,000 requests per day 250 seconds (4.1 minutes CPU) 20 seconds 230 seconds saved daily

✨ The Full Optimize Command (Including Config)

ONE COMMAND TO RULE THEM ALL
php artisan optimize

This runs:

⚠️ WHAT BREAKS CONFIG CACHE

If you do any of these, config:cache will break:

🔍 How to Check If Your Code Is Config-Cache Safe

PRO TIP: SCAN FOR env()

Run this command in your project root to find all env() calls outside config/:

grep -r "env(" --exclude-dir=vendor --exclude-dir=storage --exclude-dir=bootstrap/cache --include="*.php" | grep -v "config/"

If you see any results, fix them before running config:cache in production.

Migration checklist for config cache safety:

LocationUse env()?Use config()?
config/*.php ✅ YES (the only place) ❌ No (not yet built)
app/Http/Controllers/*.php ❌ NO ✅ YES
app/Models/*.php ❌ NO ✅ YES
database/migrations/*.php ❌ NO ✅ YES
resources/views/*.blade.php ❌ NO ✅ YES (config('app.name'))
app/Console/Commands/*.php ❌ NO ✅ YES

📝 Topic 3 Summary: Config Caching

ConceptWithout CacheWith Cache
.env file reads Every request (opens entire file) Never (cached)
Config files parsed 20-50 files per request 1 cached file
env() usage allowed where? Anywhere (but slow) Only in config/*.php
Command php artisan config:cache or php artisan optimize
📌 THE RULE: Never use env() outside config/*.php.
Use config() everywhere else. Run php artisan optimize after every deployment.
This saves 15-40ms per request and prevents production crashes.
NEXT TOPIC PREVIEW

Topic 4: Eloquent N+1 Killer — The #1 performance disaster in Laravel. Why 101 queries run when 2 would suffice. And how preventLazyLoading() saves your soul.