Troubleshooting
The runtime refuses things deliberately and often. Most of what looks like a fault is a refusal with a reason, so the first question is usually “what is it telling me” rather than “what is broken”.
It will not start
no database configured — RUNTIME_DATABASE_URL is unset. Every command
but model validate and model schema needs it.
RUNTIME_OIDC_ISSUER and RUNTIME_OIDC_AUDIENCE must be set together —
half-configured authentication is refused on purpose. An issuer with no audience
accepts tokens minted for every other service that provider serves.
no identity provider is configured and the model declares no anonymous role — nothing could ever reach this application. Either configure OIDC, or
give the model an auth.anonymousRole.
A flow could not be compiled — a script has a syntax error. Compiling
happens at start-up so this is a failed deploy rather than a 500 for whoever
calls that flow first. The message names the file and line.
Readiness fails
Always ask it:
$ curl "localhost:9090/readyz?verbose"
[+] ping ok
[+] database ok
[-] model failed: this database was provisioned from a different model; run `runtime db apply`
| Check | Failing means |
|---|---|
database | The pool cannot reach PostgreSQL, or the query timed out |
model | The database was provisioned from a different model |
draining | A termination signal arrived; this is normal during a shutdown |
Liveness staying 200 while readiness fails is correct, not a bug. A
restart does not create a schema or bring a database back — it only adds a cold
start to an outage already in progress.
Requests fail
401 with the bearer token is not valid — every rejection reads the same
on purpose: the difference between “expired” and “bad signature” is useful to an
attacker and to nobody else. The real reason is in the log, on the
runtime::audit target.
403 on everything — the token’s claim values map to no user role. A claim
value with no mapping in app.yaml grants nothing, which is the safe direction
to fail. Check auth.roleMapping against what your provider actually sends.
404 on a row you know exists — the row is outside your role’s constraint.
Hidden and missing are deliberately indistinguishable, because answering
differently would let anyone confirm an identifier by asking for it. Fetch
/api-doc/openapi.json with the same token to see what it can reach.
428 — PATCH and DELETE need an If-Match. Read the row first and send
back its ETag; or send If-Match: * to overwrite deliberately.
409 on a write — somebody changed the row since you read it. Re-read and
retry. This is the mechanism working.
422 naming a field you are sure exists — with the detail “is not a field of
this entity”, the field exists and your role cannot read it. That message is
identical to the one for a typo, deliberately, so an error cannot be used to
enumerate the schema.
A flow misbehaves
500 with no detail — the script called error(), or hit a bug. The
message is in the log, not the response, because a script’s author and a
script’s caller are usually different people. Use runtime.fail("...") for
anything the caller should read.
cannot assign to the global — Lua makes undeclared variables global
silently, which in a sandbox leaks state between parts of a script. Use local.
io, os, require are nil — they are absent, not restricted. A flow
cannot read a file, run a process or make an HTTP request. That is the boundary,
not a missing feature.
The script reaches no rows — it runs as whoever called it. If the caller cannot see a row, neither can the script. That is the design; see Flows.
the script used more instructions than it is allowed — a runaway loop.
Wrapping it in pcall will not help: the budget poisons the interpreter and the
runtime checks that after the call.
A metric never appears
Nothing shows up at all — metrics leave over OTLP and are exported on an interval, by default every sixty seconds. Wait, then look again.
The model will not load — a label on a string, decimal or datetime is
refused. Only enum and boolean may be labels; see
Metrics for why, and what to do instead.
It reads zero forever — check on:. A metric with an empty on list counts
nothing, and validation warns about it (M904).
Reload refuses
outcome | Meaning |
|---|---|
schema-changed | The model needs a migration. Run runtime db apply and deploy. |
invalid | The bundle does not validate, or a script does not compile. |
Both leave the running model untouched, which is the point of validating before swapping rather than after.
Nothing above fits
runtime config prints what configuration actually took effect.
runtime model validate reports every problem in a bundle at once. Between
them, most “it does not work” turns into a specific message — and if that message
is unclear, that is a bug worth reporting.