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). This page is the contract your integration is built against: exactly what response you get during a window, and how to handle it safely.

Written for the Shared plan. This contract describes a Shared-plan instance. A Dedicated instance is stopped outright for the whole of its window, so what you get differs: during your instance's own upgrade window the API answers 503 with Retry-After and one of the code values below, and the site answers a bare 503 with no JSON. See Dedicated instances at the end of this page.

The short version#

Today the whole fleet moves in one window. Browser reads keep working for most of it. 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 while the new version is brought up: during that pause the instance is unavailable to everyone, reads included.

When your advance notice names a window for your instance specifically, the shape is different, and this is the shape we are moving to: instances are upgraded one at a time, so the announcement covers the whole fleet (often several hours) while your instance is read-only for a much shorter slot inside it, with its own start and finish. How long that is depends on how much data you hold: most instances are read-only for well under an hour; the largest can take up to about ninety minutes. Your advance-notice email quotes your instance's own figure; nothing on this page and nothing on the status page does.

In that shape your instance is read-only, not offline (it is never scaled away, there is no unavailable pause at the end, and nothing is deleted), and you will be signed out, twice: when your instance moves across to the upgraded servers, and again later in a short, separately announced window when we move it back onto its usual servers. Your data is not touched and the instance stays writable throughout that second move, but sessions do not survive it, and neither does an API session your integration is holding.

The 503 contract below is identical in both shapes; only the timing differs, and your advance email is what says which one applies to you.

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 for a window (see below). More codes use this same envelope for a different reason; see Not every JSON 503 is a maintenance window
windowId string or null The maintenance window's id, when known. Absent entirely on the other codes, because there is no window
retryAfterSeconds integer 300 for the two maintenance codes; 10 for the two upstream-health codes; 30 for TENANT_LOOKUP_UNAVAILABLE. Always matches the Retry-After header. Read it, do not hard-code it
windowEndsAt string or null The window's expected end time (UTC, ISO-8601), when known. Absent entirely on the other codes
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. windowId and windowEndsAt are 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, but windowId and windowEndsAt are null. 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 like MAINTENANCE_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.

Not every JSON 503 is a maintenance window#

Your instance host answers with this same envelope (same shape, same field names, same Retry-After header) in other situations that have nothing to do with a scheduled window. code is how you tell them apart, and it is the only field you should branch on.

  • UPSTREAM_UNAVAILABLE (503): your OpenBoxes instance is not answering, and the platform has stopped forwarding requests to it for a short cool-off rather than queueing them against a service that is down. retryAfterSeconds is 10, not 300. The request was not forwarded, so it was not applied and it is safe to retry. This is unplanned: it means the instance is unhealthy, not that maintenance is in progress. If it persists for more than a few minutes, contact support. There is nothing to wait out. (On a Dedicated instance this code is also what you can get during your own announced upgrade window, because the instance is stopped for it. That is expected and there is no need to contact support; see Dedicated instances.)
  • UPSTREAM_TIMEOUT (504, not 503): your instance accepted the request but did not respond within 15 seconds. retryAfterSeconds is 10. This one is ambiguous, and it is the only response on this page that is: the request may have reached OpenBoxes and been applied before the timeout, or it may not have. Treat it exactly like the timed-out request described under The guarantee, and where it stops: check whether the work landed before resubmitting it.
  • TENANT_LOOKUP_UNAVAILABLE (503): the platform could not look up which instance this address belongs to, so it could not route the request. retryAfterSeconds is 30. The request was not forwarded, so it is safe to retry. This is a short platform-side fault, not a sign that your instance is gone.
  • TENANT_NOT_FOUND (404, not 503, and with no retryAfterSeconds): there is no OpenBoxes Lift instance at this host name. Retrying will not help. Check the host name your integration is calling; your instance link is shown in the portal at app.openboxes.cloud.

Practical rules for a client:

  • Branch on code, never on the status alone. A 503 on its own no longer tells you what happened.
  • MAINTENANCE_WINDOW, MAINTENANCE_STATE_UNAVAILABLE, UPSTREAM_UNAVAILABLE and TENANT_LOOKUP_UNAVAILABLE are all safe to retry blindly after retryAfterSeconds; nothing was applied.
  • UPSTREAM_TIMEOUT (504) is not. Check first.
  • An unrecognised code should be treated as retryable-after-retryAfterSeconds but not as a guarantee that nothing was applied; we may add codes.

The Dedicated instances section at the end of this page is a different case again. During a Dedicated instance's own upgrade window the API answers 503 with Retry-After, and the body's code may be MAINTENANCE_WINDOW, MAINTENANCE_STATE_UNAVAILABLE or UPSTREAM_UNAVAILABLE. Inside the announced window, read any of them as the window, not as an incident. The advance email is how you know it is under way.

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. On a fleet-wide window the short offline pause at the end makes the instance unavailable to everyone, reads included; when the notice named a window for your instance specifically, the read-only span is the whole slot and the sign-out at the move is what interrupts a session, rather than the instance going away.
  • Any request authenticated with an API key is refused for the whole window, whatever its method, a GET included. 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 sees 503s 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 (GET with Upgrade: 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>, the link behind automatic sign-in from Launch): 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.
  • 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 a 503 carrying this JSON body was not forwarded to OpenBoxes, and can be retried after retryAfterSeconds. Nothing was applied and nothing needs to be undone. That holds for all three 503 codes (MAINTENANCE_WINDOW, MAINTENANCE_STATE_UNAVAILABLE and UPSTREAM_UNAVAILABLE) by construction: each of them is decided before the request reaches the application. It does not hold for the 504 UPSTREAM_TIMEOUT, which is a timeout and is covered by the paragraph below.

That guarantee is specifically about requests that received one of those 503s. A request that times out (whether it received a 504 UPSTREAM_TIMEOUT, or no response at all because it was sent just as a window opened) 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 503 without 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 503 that carries a JSON code after Retry-After seconds. It is always safe, because the request was never forwarded.
  • Do not do the same for a 504 UPSTREAM_TIMEOUT; it is a timeout, and the next bullet is about it.
  • 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, since 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 advance-notice email our team sends you is what tells you when a window is scheduled; scheduled windows are not listed on the status page today. When instances are upgraded one at a time, the per-instance timings live in each customer's own email, which is why your advance notice is the schedule your client should be built around.

Once the window has ended, check your own retry queue for writes that were refused during it. When your instance was upgraded in a window of its own, that includes writes refused during the moment it was being moved across, indistinguishable from the rest at the client, since both are a 503 with a Retry-After.

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

Dedicated instances#

Everything above describes the Shared plan, where your instance stays readable while a fence answers the rest with the documented 503. A Dedicated instance is upgraded differently: for the length of its window the instance is stopped.

What that means for an integration:

  • The API answers 503 with Retry-After. During your instance's own upgrade window the API answers 503 with Retry-After, reads included. Your API address (https://<your-subdomain>.openboxes.cloud/api/v1) stays routable while the instance is stopped, and the platform answers on its behalf.
  • The code may be any of the three 503 codes above. When the window is registered with the platform's fence you get MAINTENANCE_WINDOW (or MAINTENANCE_STATE_UNAVAILABLE), with Retry-After: 300. Otherwise the stopped instance cannot be reached, and you get UPSTREAM_UNAVAILABLE with Retry-After: 10, the same code an unhealthy instance returns. Inside your announced window every one of them means the window. There is no need to contact support. All three are safe to retry: nothing was applied.
  • An answer can take several seconds to arrive. The platform stops waiting for your instance after 15 seconds and then answers 504 UPSTREAM_TIMEOUT, so give your client a timeout of at least 20 seconds. Treat a 504 as the section above says: check whether a write landed before resubmitting it.
  • The site answers a bare 503, with no Retry-After and no JSON body. Pages opened in a browser get 503 Service Unavailable and no maintenance page.
  • Retry-After is a pause between attempts, not the end of the window. The window's start, length and end are in the advance-notice email; that email is the schedule your client should use. Treat any 503 during the announced window as retry-after-the-window.
  • Re-run rather than retry in a tight loop. Anything scheduled against the instance during the window should be re-run once the window has ended.
  • Nothing was applied on a 503. The instance was not serving, so no partial write can have happened: the same guarantee as above, arrived at differently. A 504 is the one exception, as above.
  • The window is not on the public status page. It is your maintenance rather than a platform event, so you are told by email and it is not announced publicly.

Webhook deliveries pause for the length of the window, for the same reason as on Shared: nothing is being written to notify you about.