Unavailable Responses
What visitors to your app see when Volcano stops serving your project's traffic, and how to detect it from a client.
When Volcano stops serving traffic for a project, every data-plane path answers
with the same status and copy: HTTP 503 Service Unavailable, and no caching.
Frontends, function invocations, storage, the database REST API, realtime, and
pgproxy all behave identically.
The response is written for your app's end users, not for you. It never mentions your account, plan, billing state, or project — a visitor cannot tell why traffic stopped. The branded HTML page does link back to Volcano's status page, but withholds every detail about your account.
Detecting it from a client
API, function, and storage callers get JSON:
{
"error": "This site is temporarily unavailable.",
"code": "service_unavailable"
}Match on code, not on the human-readable text:
const res = await fetch("https://your-site.frontends.volcano.run/api/items");
if (res.status === 503) {
const body = await res.json().catch(() => null);
if (body?.code === "service_unavailable") {
// Traffic is paused. Back off and retry.
}
}Every unavailable response carries these headers:
| Header | Value | Meaning |
|---|---|---|
Cache-Control | no-store | Never cached, so service resumes immediately once traffic is restored |
Retry-After | 60 | Suggested retry delay, in seconds |
X-Robots-Tag | noindex | Keeps the page out of search results |
Cross-origin callers
The unavailable response always carries CORS headers, so an in-page fetch()
can read the JSON body instead of failing with an opaque cross-origin error.
The request Origin is echoed (with credentials allowed) when one is present,
and * is used otherwise.
These headers are set on the unavailable response itself rather than resolved
from your project's CORS configuration, so a request from an origin your config
does not normally allow can still read this particular 503. The body contains
no information about your account, so there is nothing to disclose. A response
that already carries CORS headers — because a path resolved your project's real
policy first — keeps those headers; this only fills the gap for paths that
never resolve a policy at all. Your CORS policy continues to apply to every
other response.
Browser navigations
A top-level browser navigation receives a self-contained branded HTML page
instead of JSON. This is standard HTTP content negotiation on the Accept
header: a browser's default navigation Accept prefers text/html, while
fetch()/XMLHttpRequest and non-browser clients default to */* or
application/json and get the JSON body.
If your own page explicitly sets Accept: text/html on an in-page fetch()
call, it will receive the HTML page too — pass an explicit Accept: application/json on any request whose response your code parses as JSON.
Per-protocol behavior
| Surface | Response |
|---|---|
| Frontends | HTML page for navigations, JSON otherwise |
| Function invocation | 503 with the JSON body, regardless of Accept |
| Storage | 503 with the JSON body, regardless of Accept |
| Database REST API | 503 with the JSON body, regardless of Accept |
| Auth-user endpoints (signup, signin, refresh, profile, etc.) | 503 with the JSON body, regardless of Accept |
| Realtime | WebSocket close code 4503 |
| pgproxy | Postgres ErrorResponse, SQLSTATE 57P03 (cannot_connect_now) |
The codes above are what a new connection attempt gets. When read_only
status propagates, Volcano also force-closes already-established pgproxy
connections and realtime clients, but that teardown does not send these
codes: pgproxy drops the socket without an ErrorResponse, and realtime
closes with generic close code 3004, not 4503. Treat an unexpected
disconnect on a working connection as a signal to reconnect, then read the
refusal from the codes above on that next attempt. Neither realtime nor
pgproxy re-checks project traffic status on an already-established
connection, so a session that was still establishing when status changed
can escape the teardown sweep entirely: it keeps working, with no re-check
on its later requests, until the client or server ends that connection for
an unrelated reason — at which point the next reconnect is refused. This
can take time across instances.
Function invocation, storage, the database REST API, and auth-user endpoints
never turn a browser-issued Accept header into HTML: a script or SDK calling
these must always be able to parse the response as JSON.
Postgres drivers treat 57P03 as a retryable "server not accepting connections"
condition, so existing reconnect logic works without changes:
ERROR: This site is temporarily unavailable.
SQLSTATE: 57P03Recovery
Traffic resumes automatically once the underlying condition clears — nothing
needs to be purged or redeployed. For an account restricted by overage arrears,
Volcano requests read_only status until the monthly credit grant or purchased
credits clear the arrears, then requests active status. The allowance
resetting by itself does not resume usage while arrears remain. The no-store
response is never cached, but the
allow/deny decision at the traffic gate is cached and refreshes eventually after
the status change. A request made immediately after recovery can still receive
an unavailable response.
If you see this on your own project, check your account and project status in the Volcano dashboard. The response deliberately withholds the reason from your visitors. See plans and limits for the limits and billing behavior that apply to your project.