A Laravel 13 streaming site runs on four web servers; after php artisan down on one of them most subscribers still reach the app mid-migration; why, and how does APP_MAINTENANCE_DRIVER=cache fix it?
answer
- file driver is per server
- storage/framework/down is local
- APP_MAINTENANCE_STORE must be shared
- key illuminate:foundation:down
- cache:clear ends maintenance
basics
~20 sThe default file driver writes storage/framework/down on the one server that ran the command, so the other three stay live. APP_MAINTENANCE_DRIVER=cache stores the flag in a shared cache store, so one php artisan down covers every server.
solid answer
~40 sMaintenance state lives wherever the driver keeps it. With `APP_MAINTENANCE_DRIVER=file` (the skeleton default) it is the file `storage/framework/down`, local to each server, so `down` must run on every server or subscribers routed elsewhere keep hitting the app during the migration. With `APP_MAINTENANCE_DRIVER=cache`, `CacheBasedMaintenanceMode` puts the payload under the key `illuminate:foundation:down` in the store named by `APP_MAINTENANCE_STORE` (skeleton default `database`, falling back to `cache.default`), which every server reads. Three caveats: the store must be shared and reachable (not `file` or `array`, and a `database` store is fragile while you migrate that database); `cache:clear` or `optimize:clear` on that store deletes the key and silently ends maintenance; and the `--render` early exit no longer applies, because it checks the local file.
go deeper
Recall that the default maintenance flag is a file on each server, so down must run on every server unless the cache driver is used.
Explain APP_MAINTENANCE_DRIVER and APP_MAINTENANCE_STORE, the cache key used, and why the store must be shared.
Anticipate the traps: a database store during a database migration, cache flushes ending maintenance, and the lost --render early exit.
Decide whether multi-server maintenance windows are acceptable at all, or whether releases should avoid them through backward-compatible migrations.
## The scenario The streaming-subscription site runs four web servers behind a load balancer. For the migration window, an engineer SSHes into `web-1` and runs `php artisan down --with-secret`. Requests routed to `web-1` get the maintenance page; requests routed to `web-2` to `web-4` are served normally, and some of them write to tables the migration is rewriting. ## Why: maintenance state is per driver Laravel reads maintenance state through a **maintenance mode driver**, chosen by `config('app.maintenance.driver')`, which the skeleton sets from `APP_MAINTENANCE_DRIVER` with a default of `file`. | Driver | Where the payload lives | Scope | |---|---|---| | `file` | `storage/framework/down` on the local disk | the one server where `down` ran | | `cache` | key `illuminate:foundation:down` in a cache store | every server that uses that store | | `array` | in memory for the current process | mainly parallel testing | With the file driver, the docs are explicit: `php artisan down` has to be executed **on each server**. Deploy tools that run commands on every host can do that, but it is easy to get wrong by hand. ## The fix: the cache driver ```ini APP_MAINTENANCE_DRIVER=cache APP_MAINTENANCE_STORE=redis ``` `MaintenanceModeManager::createCacheDriver()` builds a `CacheBasedMaintenanceMode` on the store named by `app.maintenance.store`, or on the default cache store when that is empty. `activate()` puts the payload under `illuminate:foundation:down`, `active()` checks whether the key exists, and `deactivate()` forgets it. One `php artisan down` on any server, and one `php artisan up`, now affect all four. ## Caveats a senior answer names 1. **The store must be shared.** A `file` or `array` store is still local to each server, so the problem returns. Choose a store every web server reaches. 2. **Mind the database store during a database migration.** The skeleton's `APP_MAINTENANCE_STORE` default is `database`. If the migration makes that database unreachable or locks the cache table, the maintenance check itself fails and requests error out instead of seeing the maintenance page. A separate store such as Redis avoids depending on the thing being migrated. 3. **Flushing the store ends maintenance.** The payload is an ordinary cache key. `php artisan cache:clear`, or `php artisan optimize:clear` (which runs `cache:clear`), on that store deletes it, and the site comes back up mid-migration. 4. **No early exit for `--render`.** The prerendered-page stub in `storage/framework/maintenance.php` checks the local file `storage/framework/down`. With the cache driver that file is absent, so every request boots the framework and the middleware serves the template. The page works, but a half-updated `vendor/` directory can break it. 5. **Every process must share the config.** Queue workers and scheduled tasks consult the same driver, so they see the flag only if their environment uses the same driver and store. ## A runbook for the window 1. Before the window, set `APP_MAINTENANCE_DRIVER=cache` and point `APP_MAINTENANCE_STORE` at a store outside the database being migrated, then deploy so every server and worker uses it. 2. Run `php artisan down --with-secret` once, from any server or a deploy host. 3. Confirm every server now answers 503 and that workers have stopped taking jobs. 4. Run the migration. 5. Verify through the bypass cookie, then run `php artisan up` once. ## Checking it - After `down`, request the site through the load balancer several times, or hit each server directly; all should return 503. - Keep the bypass secret handy to verify the migrated app before `php artisan up`. - Remove `cache:clear` and `optimize:clear` from any script that can run during the window. ## Summary - `file` driver: per-server flag, run `down` everywhere. - `cache` driver: one shared flag, run `down` once. - The shared store must be reachable during the maintenance you are doing, and nobody may flush it.
- Why can php artisan optimize:clear bring a Laravel site out of maintenance when the cache driver is used?`optimize:clear` runs `cache:clear`, which flushes the default cache store. If the maintenance store is that same store, the `illuminate:foundation:down` key is deleted, `active()` returns false, and requests are served normally even though nobody ran `php artisan up`.
- Is APP_MAINTENANCE_STORE=database a good choice while migrating that same database?It is risky. Every request's maintenance check reads the cache table in that database, so if the migration makes the database unreachable or locks the table, the check fails and visitors get errors instead of the maintenance page. A store outside the database under maintenance, such as Redis, keeps the flag readable.
saying these in an interview costs you the question
- php artisan down on one server takes every server down
- The cache driver works with the file cache store across servers
- Clearing the application cache cannot affect maintenance mode
- --render's early exit works the same with the cache driver
- APP_MAINTENANCE_DRIVER defaults to cache in new apps