tursodatabase / tursodatabase/libsql-client-ts
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)
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 576
- Forks
- 69
- PR merge metrics
- No merged PRs in 30d
Description
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/client0.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-1group) 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@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 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
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)
- A readiness signal — expose database status (
provisioning→ready) on the Platform API, or don't letGET /databases/{name}return200until the data-plane endpoint serves. - Make the not-ready response unambiguous — return a distinct, retryable status/code (e.g.
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.) - Make
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 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the linked secrets-free repro.mjs and confirm the transient 404 after database creation. Then trace @libsql/client's HTTP 404 handling and existing error types; done should make the not-ready case distinguishable from a permanently missing database without relying on string matching, with coverage for the reproduced create-to-serve gap.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- api, backend-api-design
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100