Maintenance Windows
Lift periodically upgrades the hosted OpenBoxes version in an announced maintenance window (see Instance Management for what a window looks like in the portal, and the status page for live windows). This page is the contract your integration is built against: exactly what response you get during a window, and how to handle it safely.
The short version#
Browser reads keep working for most of the window. API calls — any request authenticated with an API key, including a read — receive a 503 with a retry hint until the window ends. At the very end of the window there is a brief offline pause, a few minutes, while the new version is brought up: during that pause the instance is unavailable to everyone, reads included.
Rollout. The machine-readable
503described on this page is being switched on. Until it is live for your instance, a write attempted during a window fails with an error from OpenBoxes rather than the clean JSON body below. The timing guidance — when a window runs, what is refused, and that a refused write was never applied — is true either way.
The 503 response#
A request that is fenced during a window is refused before it reaches OpenBoxes, with:
HTTP/1.1 503 Service Unavailable
Retry-After: 300
Cache-Control: no-store
Content-Type: application/json
{
"code": "MAINTENANCE_WINDOW",
"windowId": "6f2c9a3e-...",
"retryAfterSeconds": 300,
"windowEndsAt": "2026-09-07T02:00:00Z",
"correlationId": "b4b6a1f0-...",
"message": "This request was not forwarded and can be retried after the maintenance window."
}
| Field | Type | Description |
|---|---|---|
code |
string | MAINTENANCE_WINDOW or MAINTENANCE_STATE_UNAVAILABLE — see below |
windowId |
string or null |
The maintenance window's id, when known |
retryAfterSeconds |
integer | Always 300. Matches the Retry-After header |
windowEndsAt |
string or null |
The window's expected end time (UTC, ISO-8601), when known |
correlationId |
string | Quote this back to support if you need to ask about a specific refused request |
message |
string | Human-readable restatement of the guarantee below |
The two codes#
MAINTENANCE_WINDOW— a maintenance window is confirmed open for your instance.windowIdandwindowEndsAtare populated when the platform knows them.MAINTENANCE_STATE_UNAVAILABLE— the platform could not establish whether a window is open, and refuses to guess. The response shape is identical, butwindowIdandwindowEndsAtarenull. This is intentionally rare and fails on the safe side: if we cannot confirm a write is safe, we do not forward it. There are two ways to reach it: the internal window-state lookup failed (and no recent answer was still cached), or your instance resolved but the platform could not determine which database it belongs to. Both are platform-side faults, and both are retryable exactly likeMAINTENANCE_WINDOW.
Both codes get the same 503 status, the same Retry-After: 300, and the same guarantee below — your client does not need to branch on code to retry correctly. It is there for your logs and for support diagnosis.
What gets fenced#
- Browser sessions (a customer signed in through the portal-launched OpenBoxes UI) can still read —
GET,HEAD,OPTIONS— for the read-only part of the window. During the short offline pause at the end of the window the instance is unavailable to everyone and reads stop too. - Any request authenticated with an API key is refused for the whole window, whatever its method — a
GETincluded. Authenticating a key exchanges it for an OpenBoxes session behind the scenes (see Authentication), and that exchange is itself a database operation, so it is treated as unsafe during a window. In practice: your integration sees503s for the whole window, not just for the writes it attempts. - Every write —
POST,PUT,PATCH,DELETE— from a browser or an API key, is refused. - Two more request shapes are refused even though they arrive as a
GET:- a WebSocket upgrade (
GETwithUpgrade: websocket) — it opens a long-lived channel whose whole purpose is to act, so it is fenced like a write rather than let through as a read; - a single-use instance launch link (
GET /sso/<token>) — following it would spend the one-shot token to sign in against a database that is refusing writes, leaving the customer with an error and a used-up link. Refusing it leaves the link intact for after the window.
- a WebSocket upgrade (
- Work OpenBoxes starts on its own — scheduled jobs, background report refreshes — does not pass through this fence at all. It is covered by the database-level write barrier instead, which is what makes the window safe rather than merely quiet; you cannot observe it through the API.
The guarantee, and where it stops#
A request that received this 503 was not forwarded to OpenBoxes, and can be retried after retryAfterSeconds (Retry-After: 300). Nothing was applied and nothing needs to be undone — that is true for every response carrying this JSON body, by construction: the fence answers before the request ever reaches the application.
That guarantee is specifically about requests that received the 503. A request that times out mid-flight — sent just as a window opened, receiving neither a 503 nor a normal response — is genuinely ambiguous. The platform cannot tell you whether it was applied before the fence came up or lost after it. Do not treat a timeout the same as a 503.
Two shapes of request see the refusal in a less friendly form, and both are still refusals, not applied writes:
- A large upload (a big multipart
POST) is refused before its body is read, so the connection may be cut off rather than delivering the JSON body above. Treat a reset during an announced window as a refusal, and retry it after the window. - A cross-origin browser request gets the
503without CORS headers, so the browser reports an opaque network error rather than the status code. Server-to-server integrations — which is how the API is meant to be used — are unaffected.
Recommendation#
- Retry any
503afterRetry-Afterseconds — it is always safe, because the request was never forwarded. - For a request that timed out rather than returning a clean response, don't blindly resubmit it: check whether it already landed (look up the record by whatever natural key you sent) before retrying, or design your write path so a retry with the same client-generated identifier is a no-op if it already succeeded. This is ordinary idempotent-retry practice, not something maintenance windows introduce, but it is the case where it actually matters — most other Lift error responses are unambiguous about whether the request was applied.
Sign-in and API-key session minting#
The same response covers a browser sign-in attempt and API-key session minting during the window — both would otherwise write. If your integration authenticates a fresh API key request rather than reusing a warm session, expect the 503 for that too until the window ends.
After the window#
The maintenance-window advance-notice email and the status page tell you when a window is scheduled and when it closes. The completion notice sent after the window finishes also reports how many write attempts were refused during it, if any.
Read that number as a signal, not as a checksum. It counts attempts, not distinct operations: one integration retrying the same POST five times is five refusals, and it includes attempts from people using the instance in a browser as well as from your integration. It is a prompt to check your own retry queue — never a target to reconcile against.
See also#
- Instance Management — what a maintenance window looks like from the portal
- Authentication — how an API key is exchanged for an OpenBoxes session
- Rate Limits — the other retryable status code (
429) your integration should already handle