Maintenance Mode
When you deploy a release, run a long migration, or need to take the app offline for a moment, put it into maintenance mode. Every request is short-circuited with a 503 Service Unavailable until you bring it back up — with an escape hatch for yourself and any health-check endpoints you nominate.
This is Rudder's equivalent of Laravel's artisan down / artisan up.
Taking the app down
pnpm rudder downThat's it — the app now returns 503 to every visitor. Bring it back with:
pnpm rudder upBoth commands are skip-boot: they don't start the application (writing a flag file needs no app context — and the app may well be down precisely because it can't boot). They write and remove a JSON flag file at storage/framework/down; the kernel checks for it on every request.
rudder down options
| Option | Description |
|---|---|
--secret <token> |
Bypass token. Visit any URL with ?secret=<token> once to set a bypass cookie and browse the app normally while it's down. |
--retry <seconds> |
Value for the Retry-After response header. Tells well-behaved clients and crawlers when to come back. |
--message <text> |
Message shown in the 503 response body. Defaults to "Service Unavailable". |
--allow <paths> |
Comma-separated paths always let through — e.g. health checks. Each entry supports a trailing * wildcard. |
pnpm rudder down \
--message "Upgrading the database — back in a few minutes" \
--retry 120 \
--secret "let-me-in-9f3a" \
--allow "/health,/status,/webhooks/*"While down, this configuration:
- returns
503with{ "message": "Upgrading the database — back in a few minutes" }, - sets
Retry-After: 120on every503, - lets
/health,/status, and anything under/webhooks/through untouched, - lets you through if you visit any URL with
?secret=let-me-in-9f3a.
Bypassing maintenance mode
Pass --secret when taking the app down, then hit any URL with that token in the query string:
https://example.com/?secret=let-me-in-9f3aThe first such request sets an HttpOnly, SameSite=Strict cookie (rudder_maintenance_bypass), so every subsequent request from that browser passes through normally — no need to keep the query string. The rest of the world still sees the 503.
What's always let through
Even with no --allow list, the maintenance middleware never gates:
- Vite internals and static assets — paths starting with
/@or whose last segment contains a dot (/app.css,/@vite/client). This keeps the dev overlay and HMR socket alive if you runrudder downduring local development. - Allow-listed paths — the union of
--allowand anyexceptpaths configured on the middleware. - Bypass requests — a valid
?secret=query param or the bypass cookie.
How it works
A JSON flag file at storage/framework/down is the single source of truth:
{
"time": 1717953383000,
"message": "Upgrading the database — back in a few minutes",
"retry": 120,
"secret": "let-me-in-9f3a",
"allow": ["/health", "/status", "/webhooks/*"]
}The kernel auto-installs maintenanceMiddleware first in the request pipeline — before any of your global or group middleware. When the app is up it's a pure existsSync no-op, so there's no measurable cost to leaving it in place. When the file exists, it short-circuits the request as described above. There's nothing to register; it's wired automatically.
Programmatic access
The same primitives are exported from @rudderjs/core (Node-only) if you need them in a script or a custom command:
import {
down,
up,
isDownForMaintenance,
maintenanceData,
} from '@rudderjs/core'
down({ time: Date.now(), message: 'Scheduled maintenance', retry: 300 })
isDownForMaintenance() // → true
maintenanceData() // → { time, message, retry, ... } | null
up() // → true if it was down, false if already upScheduled tasks during maintenance
By default the task scheduler skips every task while the app is down — both schedule:run and the schedule:work daemon honor the flag file. A task that must keep running regardless (a heartbeat, a critical cleanup) opts back in with evenInMaintenanceMode():
schedule
.call(sendHeartbeat)
.everyFiveMinutes()
.evenInMaintenanceMode() // runs even while `rudder down`
.description('External heartbeat')See Task Scheduling for the full set of schedule hooks.