Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 configuredRUNTIME_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`
CheckFailing means
databaseThe pool cannot reach PostgreSQL, or the query timed out
modelThe database was provisioned from a different model
drainingA 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.

428PATCH 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

outcomeMeaning
schema-changedThe model needs a migration. Run runtime db apply and deploy.
invalidThe 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.