Summary
A query to a freshly-created Turso Cloud database throws LibsqlError: SERVER_ERROR: Server returned HTTP status 404 for ~2.5 seconds after the Platform API has acknowledged the create and the control plane reports the database as existing. There is no readiness signal to wait on, and the 404 is byte-identical to the one a database that never existed returns — so it's indistinguishable from "database not found".
This is the natural sequel to #189 ("Confusing error message when database URL does not exist"): the same 404 is also returned for a database that does exist but whose libSQL endpoint isn't serving yet.
The root cause looks platform-side (control plane vs. data plane), but it surfaces entirely through @libsql/client, and there's a concrete client-side ask below, so filing here — happy to move if there's a better tracker.
Environment
@libsql/client 0.17.3, Node v22.22.0
- Turso Cloud, database-per-database in a group (created via Platform API
POST /v1/organizations/{org}/databases)
- Reproduced both in-region (Vercel
iad1 ↔ aws-us-east-1 group) and cross-region (see Region below)
The error
LibsqlError: SERVER_ERROR: Server returned HTTP status 404
(cause: HttpServerError, status 404)
Reproduction
Minimal, self-contained, secrets-free: https://github.com/ejc3/turso-create-serve-404
npm install
TURSO_API_TOKEN=… TURSO_ORG=… TURSO_GROUP=… TURSO_GROUP_AUTH_TOKEN=… node repro.mjs
For each fresh db it samples both planes against one clock, and queries the data plane both at control-exists (data@control) and after the create POST returned 200 to the caller (data@post-ack). Reproduces 6/6–10/10 every run; each db is deleted afterward:
baseline: data-plane query to a NEVER-created db → 404 (same HTTP 404 as the create→serve case)
# 1 reprotest-a483aa0d: POST-ack@465ms | control "exists"@144ms → data@control=404 | data@post-ack=404 | data serves@2504ms | gap=2360ms ← create→serve 404
# 2 reprotest-ec2257b2: POST-ack@399ms | control "exists"@124ms → data@control=404 | data@post-ack=404 | data serves@2458ms | gap=2334ms ← create→serve 404
# 4 reprotest-f2b422cc: POST-ack@326ms | control "exists"@226ms → data@control=404 | data@post-ack=404 | data serves@2447ms | gap=2221ms ← create→serve 404
...
RESULT: 6/6 reproduced the create→serve 404 (data plane 404'd at control-exists and/or after POST-ack, AND the db later served)
gap (control "exists" → data serves): min=2189ms median=2342ms max=2360ms
A reproduction is counted only when the data plane 404s and the same database later serves — so wrong credentials / permanent unavailability cannot be a false positive.
Mechanism (and why it is NOT a client read-before-write)
Sampling both planes from one t0 (just before the create POST):
| signal |
when |
control plane GET /databases/{name} first returns 200 ("exists") |
~75–230ms |
POST /databases returns 200 to the caller (create ACKed) |
~260–470ms |
a data-plane SELECT 1 at the instant the control plane said "exists" (data@control) |
404 |
a data-plane SELECT 1 after the POST returned 200 to the caller (data@post-ack) |
404 |
| the data-plane endpoint first actually serves |
~2.4–2.6s |
| gap (control "exists" → data serves) |
~2.2–2.5s |
Two independent witnesses rule out "the client queried before its write landed":
data@control runs after the control plane confirmed the db exists — Turso has durably registered the create — yet the data plane 404s.
data@post-ack runs after POST /databases returned 200 to the caller — the write call has demonstrably completed for the client — and it still 404s for ~2s more.
In both cases the same hostname + same credentials 404s and then serves with no change on the caller's side. That is a control→data serve/propagation gap inside Turso.
And per the baseline line: a query to a never-created database returns the same HTTP 404, so the error alone can't distinguish "not ready yet" from "doesn't exist" (cf. #189).
Region
- Cross-region (client outside the group's region): gap ~2.2–2.5s (table above).
- In-region (Vercel
iad1 against an aws-us-east-1 group): the control plane reports "exists" within ~20ms; the gap is shorter but still reliably hit — a fan-out of cold readers that connect at the "exists" signal saw 62/80 queries 404 across 7/8 fresh databases in iad1.
The window exists in- and cross-region; it's merely larger the farther the reader is from the group.
Impact
The database-per-tenant / per-session / per-agent pattern (which Turso Cloud is marketed for) creates a fresh database on the hot path. Any reader that connects based on the control plane's "exists" signal — a separate process, a cold serverless instance, a second request — lands in this window and 404s on the first query of a brand-new database.
Suggested fixes (any one helps)
- A readiness signal — expose database status (
provisioning → ready) on the Platform API, or don't let GET /databases/{name} return 200 until the data-plane endpoint serves.
- Make the not-ready response unambiguous — return a distinct, retryable status/code (e.g.
425/409/a provisioning error) instead of a bare 404 that collides with "does not exist". (This part is actionable in @libsql/client: surface a distinct error type for the not-ready/404 case so callers can retry without string-matching.)
- Make
POST /databases not ACK until the endpoint serves (synchronous create), so create completion is the readiness signal.
Current workaround
After opening a connection to a freshly-created database, probe SELECT 1 with bounded exponential backoff, retrying only the 404 / unresolved-host transients, before treating the connection as usable. It reliably absorbs the window — but it's a guess at an opaque, unbounded delay that a readiness signal would make unnecessary.
Summary
A query to a freshly-created Turso Cloud database throws
LibsqlError: SERVER_ERROR: Server returned HTTP status 404for ~2.5 seconds after the Platform API has acknowledged the create and the control plane reports the database as existing. There is no readiness signal to wait on, and the 404 is byte-identical to the one a database that never existed returns — so it's indistinguishable from "database not found".This is the natural sequel to #189 ("Confusing error message when database URL does not exist"): the same
404is also returned for a database that does exist but whose libSQL endpoint isn't serving yet.The root cause looks platform-side (control plane vs. data plane), but it surfaces entirely through
@libsql/client, and there's a concrete client-side ask below, so filing here — happy to move if there's a better tracker.Environment
@libsql/client0.17.3, Node v22.22.0POST /v1/organizations/{org}/databases)iad1↔aws-us-east-1group) and cross-region (see Region below)The error
Reproduction
Minimal, self-contained, secrets-free: https://github.com/ejc3/turso-create-serve-404
For each fresh db it samples both planes against one clock, and queries the data plane both at control-exists (
data@control) and after the createPOSTreturned 200 to the caller (data@post-ack). Reproduces 6/6–10/10 every run; each db is deleted afterward:A reproduction is counted only when the data plane 404s and the same database later serves — so wrong credentials / permanent unavailability cannot be a false positive.
Mechanism (and why it is NOT a client read-before-write)
Sampling both planes from one
t0(just before the createPOST):GET /databases/{name}first returns200("exists")POST /databasesreturns200to the caller (create ACKed)SELECT 1at the instant the control plane said "exists" (data@control)404SELECT 1after thePOSTreturned 200 to the caller (data@post-ack)404Two independent witnesses rule out "the client queried before its write landed":
data@controlruns after the control plane confirmed the db exists — Turso has durably registered the create — yet the data plane 404s.data@post-ackruns afterPOST /databasesreturned 200 to the caller — the write call has demonstrably completed for the client — and it still 404s for ~2s more.In both cases the same hostname + same credentials 404s and then serves with no change on the caller's side. That is a control→data serve/propagation gap inside Turso.
And per the
baselineline: a query to a never-created database returns the same HTTP 404, so the error alone can't distinguish "not ready yet" from "doesn't exist" (cf. #189).Region
iad1against anaws-us-east-1group): the control plane reports "exists" within ~20ms; the gap is shorter but still reliably hit — a fan-out of cold readers that connect at the "exists" signal saw 62/80 queries404across 7/8 fresh databases iniad1.The window exists in- and cross-region; it's merely larger the farther the reader is from the group.
Impact
The database-per-tenant / per-session / per-agent pattern (which Turso Cloud is marketed for) creates a fresh database on the hot path. Any reader that connects based on the control plane's "exists" signal — a separate process, a cold serverless instance, a second request — lands in this window and 404s on the first query of a brand-new database.
Suggested fixes (any one helps)
provisioning→ready) on the Platform API, or don't letGET /databases/{name}return200until the data-plane endpoint serves.425/409/aprovisioningerror) instead of a bare404that collides with "does not exist". (This part is actionable in@libsql/client: surface a distinct error type for the not-ready/404 case so callers can retry without string-matching.)POST /databasesnot ACK until the endpoint serves (synchronous create), so create completion is the readiness signal.Current workaround
After opening a connection to a freshly-created database, probe
SELECT 1with bounded exponential backoff, retrying only the 404 / unresolved-host transients, before treating the connection as usable. It reliably absorbs the window — but it's a guess at an opaque, unbounded delay that a readiness signal would make unnecessary.