Release B serves /inventory and labels its catalogue green. The code is
correct. The environment is correct. Yet the application answers /catalogue
with blue, while /inventory returns 404.
Nothing corrupted Laravel’s generated files. They contain release A’s configuration and route table because release A created them. The deployment combined those files with release B and produced a system which is consistently wrong.
config:cache and route:cache are often introduced as performance switches.
That description misses their more important property: each command turns a
set of inputs into a generated artifact. If the artifact is deployed, it must
carry the same release identity as the code and environment which produced it.
A generated cache belongs to one release
Laravel’s config:cache command boots a fresh application,
collects its configuration, and writes the result to the configured cache
path. Its route:cache command boots a fresh application,
prepares the registered routes for serialization, and writes a compiled route
collection.
Those files depend on more than the command name. Their inputs include:
- the Laravel and PHP versions;
- installed packages and discovered providers;
- application configuration and route files;
- controllers, middleware, and provider registration which affect routes; and
- environment values available when configuration is built.
The synthetic LAR-02 fixture makes the dependency visible with
two releases. Both use the same controller and response schema. Release A
loads CATALOGUE_LABEL=blue and registers /catalogue. Release B loads
CATALOGUE_LABEL=green and registers /inventory. Each route graph contains
501 named controller routes.
Freshly generated artifacts preserve the expected behavior for both releases.
Release B paired with release A’s artifacts does not. The stale configuration
still returns blue; the stale route table still exposes /catalogue; and the
new /inventory route does not exist. Rebuilding the two artifacts under
release B changes their hashes, removes /catalogue, and exposes /inventory
with green.
This is why “run the cache command on the server” is not a deployment contract. The useful questions are which release ran it, which inputs it observed, which files it produced, and whether those files moved through activation and rollback with that release.
Three kinds of cache create three different risks
The word cache is doing too much work here.
First, bootstrap/cache/config.php and bootstrap/cache/routes-*.php are
generated deployment artifacts. They remove repeated bootstrap work. They can
usually be rebuilt from declared inputs.
Second, Laravel’s default cache store contains application data. Depending on the application, that may include computed values, rate-limit state, locks, or other runtime coordination. Its ownership and safe deletion rules come from the application, not from the fact that Laravel calls it a cache.
Third, the operating system and storage stack cache file contents. They affect measurements, but Artisan does not give an experiment a cold machine. A fresh PHP process on a repeatedly used Docker filesystem is still a fresh process on a warm, uncontrolled filesystem.
Collapsing these into “clear the cache” hides both the target and the blast radius. A deployment script should name the generated artifact it is replacing. An operator considering application data deletion needs a separate decision. A benchmark report needs to disclose the host state it did not control.
Bootstrap measurements do not decide the deployment
The fixture measures release A and B in four states: uncached, configuration only, routes only, and both artifacts generated. Every state keeps three trials of 30 fresh PHP requests. Artifact builds, route-list validation, and source execution counters stay outside request timing.
The generated files do remove work. Structural counters show that an uncached request executes the selected configuration and route source, while the corresponding generated artifact prevents that source from executing during the request.
The timing result is less tidy. On the retained run, release A’s median moved from 51.6 ms uncached to 37.0 ms with both artifacts. Release B moved from 39.7 ms to 39.6 ms. Their p95 values also varied with the host. The response contract, route count, process CPU, peak memory, raw samples, and filesystem disclosure remain available beside those numbers.
That is evidence for measuring the intended application, not for promising a portable percentage. Five hundred synthetic routes can make route registration visible without representing a production route graph, OPcache model, process manager, or machine. An application with a small route set may gain nothing worth operationalizing.
The deployment question therefore has two independent parts:
- Does removing this repeated bootstrap work matter on the intended workload?
- Can the deployment publish, validate, activate, and roll back the generated file with its exact inputs?
A favorable benchmark does not repair unsafe pairing. Safe pairing does not invent a meaningful performance gain.
Configuration caching freezes the observed value
Laravel’s configuration documentation says that after
configuration has been cached, the application’s .env file is not loaded;
calls to env() should therefore remain in configuration files. External
system environment values are a separate input, but application code should
read the resulting value through config().
The fixture builds release A’s configuration with CATALOGUE_LABEL=blue, then
starts a fresh PHP process with the external variable changed to green. The
controller still receives blue from config(). After config:clear, the same
release source boots under the changed environment and returns green.
The cached value is not stale because time passed. It is stale relative to a
changed input. That distinction points to the repair: rebuild the artifact from
the intended release inputs. Do not scatter env() calls through application
code in an attempt to bypass the artifact.
The fixture enforces that boundary twice. A source scan rejects env() outside
configuration, and a cached behavior probe shows that an application-level
lookup without an external variable returns null while config() retains the
built value.
Route caching preserves the old application’s map
Route caching has the same source-to-artifact relationship. The routing
documentation describes route:cache as a deployment-time way to
reduce route registration work. The generated collection does not inspect
newer route source on every request; avoiding that work is its purpose.
In the stale-pairing control, release B starts with release A’s route artifact.
route:list and the HTTP kernel agree: 501 routes are present, but the primary
route is still catalogue.primary. /catalogue returns A’s response and
/inventory returns 404. After rebuilding, both observers agree on
inventory.primary, and /catalogue returns 404.
The artifact hashes make the pairing inspectable. They are more useful than a
boolean called routes_cached because they distinguish one generated route
table from another. Record the route count and a behavior check as well: a hash
can prove identity without proving that the intended route exists.
This case does not rely on old warnings about closure routes. The pinned Laravel version accepts the route graph used by the fixture. The defect is simpler and more general: source changed while its generated representation did not.
optimize:clear crosses into application data
Laravel’s deployment documentation states that
optimize:clear removes files generated by optimize and all keys in the
default cache driver. The framework’s
OptimizeClearCommand confirms that cache:clear is part
of its command set.
The fixture seeds the file-backed application cache with a sentinel, then runs each clear command in isolation:
| Command | Generated target | Sentinel afterward |
|---|---|---|
config:clear | Compiled configuration | Present |
route:clear | Compiled routes | Present |
optimize:clear | Optimization files and default cache | Missing |
That does not make optimize:clear a bad command. It makes it a command with a
larger contract than “remove disposable deployment files.” If the default
store holds runtime coordination or expensive data, its deletion needs the
same impact analysis as any other application-state change.
Targeted commands communicate intent better during a deployment. Reserve the broader command for a case where clearing the default store is understood and acceptable.
Activation is one boundary, reload is another
An immutable release directory gives generated files a natural owner. A deployment can construct release B without changing the active release, then validate the complete candidate before traffic reaches it.
A defensible sequence is:
materialize commit and locked dependencies
materialize the intended environment
complete package discovery and provider-affecting builds
clear disposable generated artifacts inside the candidate release
build config and route artifacts inside that release
record release, source fingerprints, commands, paths, hashes, and sizes
validate configuration, route-list, and HTTP behavior
activate code and matching artifacts together
reload long-lived processes
retain the preceding complete release until post-activation checks passThe reload step is not decoration. Switching a symlink changes which files a new process opens. It cannot change code or configuration already held in an existing web worker, queue worker, scheduler, Octane process, Horizon process, or Pulse process. The exact reload mechanism belongs to the process manager, but the deployment must contain the boundary.
Rollback follows the same rule. Activate the preceding code with the artifacts
it produced, then reload processes. Copying an old config.php into a shared
mutable bootstrap/cache directory is not the same operation.
Uncached is a valid operational choice
Generated artifacts are optional optimizations, not proof of a serious Laravel deployment. Leave configuration or routes uncached when their measured effect is immaterial, inputs change frequently during development, the route graph cannot be generated by the pinned framework, or the release process cannot pair and validate artifacts safely.
Reconsider that choice when route count, provider discovery, configuration inputs, Laravel version, PHP and OPcache behavior, release topology, or process lifecycle changes. The decision record should name the repeated work, measured distribution, artifact inputs, validation, stale behavior, reload obligation, rollback pairing, and the condition which returns to uncached operation.
The reproduction retains 720 raw requests, 20 rejected deployment-cache
mutations, seven official-source responses, two score-free artifact decisions,
and a clean export of commit
5716d67c838618c4dcee3bb36c1c0c835c40d892. It proves the behavior of its
pinned synthetic releases. It does not prove a production speedup, deployment,
filesystem, process lifecycle, or rollback.
Cache configuration and routes when the repeated work matters and the release process can own the resulting files. Otherwise, the uncached application is safer than an optimization whose inputs and activation boundary are guesses.