Skip to content

Newly-created Cloud DB returns SERVER_ERROR: Server returned HTTP status 404 for ~2.5s after the Platform API reports it exists (create→serve gap, no readiness signal) #346

Description

@ejc3

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 iad1aws-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":

  1. data@control runs after the control plane confirmed the db exists — Turso has durably registered the create — yet the data plane 404s.
  2. 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)

  1. A readiness signal — expose database status (provisioningready) on the Platform API, or don't let GET /databases/{name} return 200 until the data-plane endpoint serves.
  2. 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.)
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions