Why php artisan config:cache breaks your env() calls in production
config:cache is one of the first performance tips anyone gives a new Laravel deploy — bundle every config file into one cached PHP array so the framework doesn't re-parse and re-read the environment on every request. It works exactly as advertised for anything that goes through config(). The trap is that it also quietly changes what env() returns anywhere outside a config file, and that change is invisible until the first production deploy that actually runs the command.
ON THIS PAGE
What config:cache actually does
Without caching, Laravel reads every file in config/, and each of those files typically calls env() to pull values from the .env file — env('MAIL_HOST'), env('APP_DEBUG'), and so on. This happens on every single request, which is real, measurable overhead at scale.
php artisan config:cache collapses all of that into a single compiled file — every config value is evaluated once, right now, and the result is written to bootstrap/cache/config.php. From that point on, Laravel loads the entire configuration from that one cached array and never touches config/*.php or .env again until the cache is cleared.
That last part is the entire gotcha: once this cache exists, .env is no longer read at all.
Why env() calls outside config files return null
Laravel's own recommendation, stated directly in the config caching docs, is that env() should only ever be called from inside config/*.php files — never from application code like a controller, a service class, or a model:
// Inside a controller — this is the trap
public function isFeatureEnabled(): bool
{
return env('FEATURE_NEW_CHECKOUT') === 'true';
}
Without a config cache, this works — env() reads .env directly, every time, from anywhere. The moment config:cache runs, this call still executes, but the environment variables it depends on are no longer loaded into PHP's environment by Laravel's bootstrap process, because that entire mechanism was bypassed in favor of the pre-compiled cache. The call doesn't throw an error. It just returns null, silently, forever, until the cache is cleared.
This is exactly why it's so easy to ship: locally, you're almost never running config:cache (there's no performance need to on a dev machine), so this code path never gets exercised until it hits an environment where deploy scripts run it — typically staging or production, for the first time, on deploy day.
The fix: route everything through config(), never env(), outside config files
Define a config value once, in the relevant config file, and read that everywhere else in the app:
// config/features.php
return [
'new_checkout' => env('FEATURE_NEW_CHECKOUT', false),
];
// Anywhere in application code
public function isFeatureEnabled(): bool
{
return config('features.new_checkout');
}
config() reads from the cached array when one exists, and falls back to the live config files when it doesn't — either way, it works correctly. env() only reliably works when there's no cache, which in practice means it only reliably works in local development. Any env() call outside config/*.php is a bug waiting for the first config:cache run to surface it.
If you're auditing an existing codebase for this, a quick search is genuinely worth running before your next deploy:
grep -rn "env(" app/ --include="*.php" | grep -v "^app/.*config"
Anything that turns up outside a config context is a candidate for this exact failure mode.
A related trap: caching before .env is actually updated
Because the cache is a snapshot taken at the moment the command runs, the order of operations during deploy matters. If a deploy script updates .env and then runs config:cache, the new values get baked in correctly. If it caches first and updates .env afterward — or if someone manually edits .env on the server after a cache already exists, expecting the change to take effect immediately — the running application keeps serving the old cached values until someone explicitly re-runs config:cache (or config:clear).
This produces a particularly confusing version of the bug: the .env file on the server clearly shows the correct new value, and the application is still behaving as if it doesn't exist.
Other things that go stale on the same kind of schedule
A few related deploy-time issues worth having in the same checklist, since they follow a similar pattern of "correct until the moment a cache or long-running process falls out of sync with what was just deployed":
- OPcache holds compiled PHP bytecode in memory and won't necessarily notice new files landing on disk during a deploy, depending on server configuration. If deployed code doesn't seem to take effect at all, an OPcache reset (or
opcache_reset(), or a PHP-FPM reload) is worth checking before assuming the deploy itself failed. - Queue workers are long-running processes that load the application once and keep executing that same loaded code — a deploy without
php artisan queue:restartafterward leaves workers running the previous version, sometimes for hours, entirely separately from whatever config caching issue is happening in the web process. - Route and view caches (
route:cache,view:cache) follow the identical pattern to config caching — genuinely helpful for performance, and equally capable of silently serving stale output if cleared and rebuilt out of order relative to the actual code deploy.
A minimal, ordered deploy checklist
# 1. Pull the new code
git pull origin main
# 2. Install dependencies for the new code
composer install --no-dev --optimize-autoloader
# 3. Run migrations
php artisan migrate --force
# 4. Clear old caches BEFORE rebuilding them
php artisan config:clear
php artisan route:clear
php artisan view:clear
# 5. Rebuild caches against the current .env and current code
php artisan config:cache
php artisan route:cache
php artisan view:cache
# 6. Tell queue workers to pick up the new code
php artisan queue:restart
The clear-then-rebuild order in steps 4–5 matters — rebuilding without clearing first can, depending on what changed, mean the new cache is built from a mix of stale and fresh state rather than cleanly from the current .env and codebase.
Checklist before your next production deploy
- Does any application code outside
config/*.phpcallenv()directly, rather than reading throughconfig()? - Does the deploy script clear existing caches before rebuilding them, rather than caching on top of a stale cache?
- Is
php artisan queue:restartpart of the deploy process, so workers don't keep running old code indefinitely? - If OPcache is enabled, does the deploy process account for it — either an automatic reset on deploy, or a PHP-FPM reload?