Index

Cache the Deployment, Not the Assumption

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:

  1. Does removing this repeated bootstrap work matter on the intended workload?
  2. 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:

CommandGenerated targetSentinel afterward
config:clearCompiled configurationPresent
route:clearCompiled routesPresent
optimize:clearOptimization files and default cacheMissing

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 pass

The 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.