Skip to content

Turso, D1, Durable Objects and Supabase Postgres from one Cloudflare Worker

Which SQL store a Cloudflare Worker should talk to, for which kind of workload: Turso (libSQL, and the newer Turso engine), Cloudflare D1, a SQLite-backed Durable Object, or Supabase Postgres through Hyperdrive or PostgREST. The question became a practical one when Supabase announced it is acquiring Turso1; this page answers it with measurements from one lab.

Every figure without a footnote comes from the sqlite-edge-compare experiment in supabase-lab: three batteries, 2026-10-15, 2026-10-16 and a third on 2026-10-16 marked r3 (UTC), one probe Worker on the Singapore (SIN) colo unless a row says otherwise (in r3 the colo moved per module: SIN, Osaka KIX or Hong Kong HKG, named on each r3 figure), and one local client in Singapore, except the Turso usage figures read by hand from the Platform API, which the text labels. Supabase (ap-southeast-2, Micro) and Turso (aws-ap-southeast-2) sat in Sydney; D1 had location hint oc and reported served_by_region OC, served_by_colo MEL (2026-10-15 and the 2026-10-16 re-run) and BNE (r3); the Durable Object, hint oc, reported BNE at fixture setup (from an in-object /cdn-cgi/trace, not an artifact). Absolute latencies are therefore dominated by the round trip from Singapore; read them for their order and shape. A backend is called faster than another only where the two rows’ p25-p75 ranges do not overlap. Documented limits are cited and marked as documented; they were not all tested.

TL;DR:

  • Point reads from a Worker: Turso 184-190 ms p50, D1 221-227, Postgres through Hyperdrive 250-257, PostgREST 307-324. The libSQL and Turso-engine databases read at the same speed. Turso pays 729.5-740 ms on the first query of each Worker invocation.
  • D1 refuses BEGIN TRANSACTION from a Worker; its transaction is batch(). Turso and Postgres give a Worker interactive transactions.
  • At 64 concurrent single-statement writers, D1 and Postgres kept a per-op p50 of 253 to 329 ms (2026-10-16) and gained throughput; Turso libSQL queued (1690.5 to 2592.5 ms p50) in all three runs.
  • The Turso engine’s BEGIN CONCURRENT beat libSQL on concurrent inserts at 4 writers (4.4 against 1.83 tx/s, local client over Hrana HTTP, one sample) and retried hundreds of write-write conflicts on one hot row; on Turso Cloud it had no FTS5 and the module ran it without a vector index.
  • Hyperdrive’s cache returned the pre-write value on 29 of 30 reads after a write.
  • A Turso embedded replica reads locally in 0.21-0.31 ms; the D1 binding and the Postgres drivers used here offer no local replica (recorded, not measured). Turso Sync works on both engines; two clients that update one row offline both push without an error and the second push wins.
  • A SQLite-backed Durable Object read like D1 from SIN (p50 221 against 230 ms, ranges touching) with no first-query cost, refused BEGIN TRANSACTION as D1 does, ran FTS5, and at 64 writers reached 224.36 insert ops/s against D1’s 179.65 in the same run (r3, Worker colos KIX+HKG+SIN, one sample).
  • On the Turso engine a window-function query counted 4 rows written per input row per execution, against none on libSQL; inserts and other reads counted the same on both. On the first account that query accounts for about 9,200,000 of the Turso-engine database’s 9,639,906 rows written (derived from the probe, not re-measured), most of the organization’s 11,774,888 against the 10 million allowance.
Cloudflare (SIN colo)Turso aws-ap-southeast-2Supabase ap-southeast-2local client(Singapore)probe WorkerPOST /runHTTPSlibSQL databaseHTTP, replicaPostgrespooler, directHyperdrive(caching off / on)pgD1(hint oc)binding@libsql/client/webTurso-engine database@libsql/client/webPostgRESTfetchdirect :5432

The Worker is workload-generic: each module sends its own SQL per backend and the Worker times each driver call with performance.now(). A Worker’s clock advances only across I/O, so an operation’s time is its I/O wall time. The Postgres paths open their connection before the first timed operation; the others pay their setup inside it (see Point reads).

WorkloadMeasured lead (Worker in SIN unless the cell says local)What decides itRests on
Key lookups, config, sessionsTurso, then D1, when an invocation makes several queries; D1 when it makes one (Turso first query 729.5-740 ms, D1 221-223.5)Round trip and first-query cost; Hyperdrive caching only for data that tolerates 60 s stalenessTD02, TD07c
A queue worker’s job-state writesTurso libSQL (1565 ms per 8-statement job, 2026-10-16), then D1 (1855)Write latency per statementTD03
Many concurrent writers on one databaseD1 or Postgres, up to the 64 writers and 162.33-166.13 ops/s measured (2026-10-16)Turso libSQL queued at 64 writers in three runsTD04
Multi-statement transactions decided at runtimeTurso or PostgresD1 has only batch()TD07
Dashboard aggregates over a 100,000-row logPostgres or D1 for window functions (Postgres led on 2026-10-16, D1 on 2026-10-15); Turso libSQL led by_hour and last_24h on 2026-10-16Query shapeTD05
Full-text searchD1 or Turso libSQL (FTS5); Postgres ranks with ts_rank (top-10 overlap with D1 0.1 to 0.5, relevance not tested)FTS5 is refused on the Turso engineTD06
Vector searchpgvector (recall 1 from SIN); Turso libSQL only after measuring recall at your index arguments (0.245 at the module’s)Recall at the chosen index argumentsTD06
A database per tenantPostgres schema (1012.95 ms), D1 (2177.16 ms), Turso (4080.64-4651.5 ms) burst p50 to first query, from the local machine (2026-10-16)Provisioning path, plan ceilingsTD01
State per entity (a tenant, a room, a user) that one Worker coordinatesA Durable Object: a named object per entity, created on first access (burst p50 to first query 1623.38 ms via the Worker, r3; D1 2289.56 in the same run)One object handles requests one at a time (documented soft limit 1,000 requests per second per object2, not measured); BEGIN TRANSACTION refused in sql.exec, transactions run inside the object via transactionSyncTD01, TD07 (r3)
Reads with no networkTurso embedded replica (local machine)Turso’s embedded replica; D1 and Postgres drivers used here recorded as having none (TD08e, not measured)TD08

The choice in each row holds for one colo, one region and one client; the absolute figures do not travel.

Primary-key read of one row from a 10,000-row table, n = 300 per row (hot-set row n = 100) after 20 warm-up iterations dropped, ms:

Path2026-10-15 p50 (p25-p75)2026-10-16 p50 (p25-p75)r3 (SIN) p50 (p25-p75)
Worker -> Turso libSQL190 (184-194.25)184 (180-189)192 (188-199)
Worker -> Turso engine-186 (183-188)195 (192-201)
Worker -> Durable Object--221 (216-225)
Worker -> D1 binding227 (224-232)221 (219-223.25)230 (225-234)
Worker -> Hyperdrive (caching off) -> Postgres257 (251-284)250 (249-282)283 (257-287)
Worker -> PostgREST307 (302-324)324 (290-327)370.5 (347-381.25)
Worker -> Hyperdrive (caching on), hot set of 10 ids5 (4-10.25)1 (1-2)5 (3-8)
local -> Supavisor transaction mode (6543)252.29 (244.47-259.13)255.57 (246.12-269.23)253.34 (245.03-265.59)
local -> Turso over HTTP257.9 (252.23-282.2)265 (256.51-306.06)264.67 (256.47-299.95)
  • Turso’s first query in each Worker invocation cost 740 ms (2026-10-15) and 729.5 ms (2026-10-16) at p50 against 189 and 183 for the rest: the client sets up its connection inside the first query (r3: 755 against 191). A Worker that serves one query per request pays this every time. The Durable Object showed no such cost (r3: 231.5 first against 220 for the rest); its range touches D1’s, so neither is faster by the p25-p75 rule.
  • From the Worker the Postgres connection through Hyperdrive opened in 7 ms (2026-10-15) and 2 ms (2026-10-16), so excluding it from the operation timings changes little. From the local machine the pooler connect took 1640.4 ms and the direct one 2197.86 ms (2026-10-15), 2015.81 and 2123.34 ms (2026-10-16).
  • Both D1 and Turso refused the 10,001st query in one Worker invocation (D1: Too many API requests by single Worker invocation; Turso: Too many subrequests by single Worker invocation) after 10,000 succeeded, on this account. The D1 limits page documents 1,000 queries per invocation on Workers Paid3; which plan figure applies to this account was not settled.

A job: insert, three status updates, a read after each, 8 statements in all. Worker rows ran from SIN on 2026-10-16; n = 100 jobs after 10 warm-up:

PathInsert p50Update p50Read p50Whole job p50 (p25-p75)
Worker -> Turso libSQL2012011861565 (1540.5-1629.25)
Worker -> D12412392241855 (1844.75-1869.25)
Worker -> Turso engine318.53191862002.5 (1981-2041.25)
Worker -> Hyperdrive -> Postgres2832832822262 (2002-2272)
Worker -> PostgREST3273223202541.5 (2469-2639.5)

Every read saw the preceding write on every backend. The Turso engine’s insert and update p50 (318.5 and 319 ms) were about 1.6 times libSQL’s (201 and 201) on the same group and client; its reads matched (186). The PostgREST row lost 80 operations to client-side timeouts that day (one block, re-sent).

256 single-statement writes per cell, spread over parallel Worker invocations of up to 4 lanes. ops/s at 1 / 4 / 16 / 64 concurrent writers, 2026-10-16 00:07 run (every cell from SIN):

BackendInserts ops/sHot-row increments ops/sPer-op p50 at 64 (insert / increment)
D14.2 / 15.82 / 55.36 / 162.334.07 / 15.64 / 61.88 / 166.13329 / 296 ms
Turso libSQL4.77 / 18.48 / 50.77 / 24.44.74 / 18.25 / 49.42 / 24.12592.5 / 2502 ms
Turso engine3.19 / 12.93 / 14.28 / 13.033.15 / 14.55 / 13.95 / 10.984575.5 / 5500 ms
Postgres via Hyperdrivefailed cell / 14.75 / 56.78 / 152.473.51 / 14.48 / 53.55 / 155.25253 / 280 ms
  • No backend lost an update in any of three runs (2026-10-15 06:10, 2026-10-15 23:56, 2026-10-16 00:07). Two cells returned an error after their rows had committed: D1 inserts at 1 writer in the 23:56 run (a Cloudflare HTTP 502 after 29617 ms, 122 rows committed) and Postgres inserts at 1 writer in the 00:07 run (a client timeout, 256 rows committed).
  • Turso libSQL’s drop at 64 writers repeated in all three runs (24.1 to 30.96 ops/s, per-op p50 1690.5 to 2592.5 ms); the last two ran entirely from SIN, so it is not the colo mix. The run cannot tell libSQL’s single writer from a per-database limit in front of it.
  • D1 never answered with its documented overload error (D1 DB is overloaded. Too many requests queued.4); these cells are bounded by the round trip and do not find where D1 starts refusing.

The Turso engine is Turso’s Rust rewrite of SQLite, “in early preview” on Turso Cloud5, with MVCC concurrent writes opened by BEGIN CONCURRENT6. Creating one through the Platform API needs an organization setting: with it off, use_tursodb: true answered HTTP 400 tursodb is not enabled for organization (2026-10-15); with it on, the database was created in 1317 ms. On Cloud it reported journal_mode mvcc and refused PRAGMA journal_mode = wal (SQL not allowed statement).

@libsql/client lists concurrent writes as not supported7, so the module drove the transactions over Hrana HTTP itself, one statement per round trip from the local machine, 64 transactions per cell, up to 25 attempts:

Engine, beginInserts tx/s at 1 / 4 / 16One-row read-modify-write tx/s at 1 / 4 / 16Conflicts retried (rmw at 4 / 16)Committed (rmw at 16)
Turso engine, BEGIN CONCURRENT1.08 / 4.4 / 1.730.9 / 1.13 / 1.24189 / 71353 of 64
libSQL, BEGIN IMMEDIATE1.31 / 1.83 / 1.820.99 / 1.26 / 1.240 / 064 of 64

Concurrent inserts at 4 writers committed 2.4 times faster with BEGIN CONCURRENT; at 16 the advantage was gone (one sample per cell). On one hot row every conflict came back as [SQLITE_ABORT] Tursodb error: Write-write conflict and was retried; the final counter equalled the committed count, and 11 of 64 transactions ran out of attempts. libSQL serialised the same work with no conflicts. Over Hrana HTTP, Turso Cloud returned no baton after an autocommit statement on both engines, so a hand-rolled stream continues only inside a transaction; @libsql/client opens a new stream instead.

BehaviourD1Turso (both engines)Postgres via Hyperdrive
Interactive transactionrefused: To execute a transaction, please use the state.storage.transaction() ...; batch() committed in 247 ms (2026-10-15) and 244 ms (2026-10-16)committed (libSQL 584 ms on 2026-10-15, 576 and Turso engine 634 on 2026-10-16)committed, 1140 ms (2026-10-15), 1137 ms (2026-10-16)
Rollback on a duplicate keybatch leaves 0 rowsbatch and interactive leave 0 rowsbatch and interactive leave 0 rows
Read-your-write in the next request30/3030/3030/30; through the caching Hyperdrive config 1/30 fresh, 29 stale
A second writer during an open transactionbatch: reads equal, slowest second write 261 ms (2026-10-16)slowest write 1934 ms (libSQL) and 2107 ms (Turso engine) on 2026-10-16, consistent with waiting (inferred, n = 1), reads equalREAD COMMITTED saw the new commits; REPEATABLE READ failed with 40001

D1 documents batch() as statements run in sequence and rolled back as a whole8. Hyperdrive documents that writes do not invalidate cached results and that the default max_age is 60 s9; the 29 stale reads in 30 are consistent with that. @libsql/client 0.18.0 maps transaction("write") to BEGIN IMMEDIATE and has no BEGIN CONCURRENT mode (read from the library source, not measured), so a Worker gets the Turso engine’s concurrency only through a client that sends the statement itself.

100,000 events, five dashboard queries, p50 ms from the Worker on 2026-10-16 (query order rotated per iteration; n = 20):

BackendIngest, summed op timeby_hourtop_pathsstatus_by_countryp95_by_cololast_24h
D136215 ms301365395630.5228
Turso libSQL33205 ms211.5236.5271.5896.5187
Turso engine43090 ms402.5493.54772413.5195
Postgres via Hyperdrive64509 ms301304306480282

Every aggregate that answered returned the correct answer; one Turso-engine last_24h read answered HTTP 520. On Turso libSQL, whichever query ran first in the invocation took 443.5 to 586 ms longer than when it did not (by_hour 756.5 against 208.5): the same first-query cost as in point reads. With that removed (not-first p50 208.5, 220.5, 265.5 and 186.5 ms on libSQL against 301, 275.5, 306 and 284 on Postgres), libSQL had the lowest p50 on the four light queries; by the p25-p75 rule it was faster than Postgres only on by_hour and last_24h. On p95_by_colo, Turso libSQL (896.5) was slower than D1 and Postgres, and the Turso engine (2413.5) slowest of all. On 2026-10-15 D1 was faster than Postgres on p95_by_colo (467, p25-p75 454.5-476.5, against 485, 480.5-489).

10,000 documents for full-text, 10,000 x 384 vectors, n = 20 per query, from the Worker on 2026-10-16:

BackendFull-text query p50 ms (4 shapes)Vector loadANN p50 ms, recall@10Exact top-10 p50 ms
D1 (FTS5)226-246.5no vector type: no such function: vector32--
Turso libSQL (FTS5; libsql_vector_idx)191-204202908 ms, index maintained on insert201.5, 0.245207.5
Turso enginerefused: Virtual tables are not supported in MVCC mode75052 ms, no index created-211, recall 1
Postgres (tsvector + GIN; pgvector HNSW)269.5-283.595615 ms + 2728 ms HNSW build257, 1259
  • Full-text match counts were identical on D1, Turso libSQL and Postgres, and libSQL’s top 10 equalled D1’s on every query.
  • The libSQL recall of 0.245 (0.265 on 2026-10-15) belongs to the index arguments the module chose (compress_neighbors=float8, max_neighbors=16), picked because the default arguments took 48101 ms for 500 rows against 6056 ms for the chosen arguments’ first 5 ops (2026-10-15); it does not rate libSQL vector search in general.
  • pgvector’s HNSW recall@10 over four full runs: 0.95 (2026-10-15, NRT colo), 1, 1, 1.
  • The Turso engine documents a Tantivy-backed USING fts index distinct from FTS510. A standalone probe on a throwaway database (2026-10-16, not a module row) found USING fts and USING vector both refused on Cloud with index method is an experimental feature, and libsql_vector_idx refused, which is why the module loaded the Turso engine without a vector index.
  • Colo matters more than backend here: a superseded 2026-10-15 run that landed in Tokyo (NRT) reversed the full-text order (Postgres 104-115 ms, Turso 123.5-134.5, D1 201.5-212.5).

Create a database (or a Postgres schema), get credentials, run a first query; one single tenant, then a burst of 20 in parallel, from the local machine on 2026-10-16:

BackendSingle: create to first queryBurst of 20: create to first query p50 (max)
Postgres schema on one project987.94 ms1012.95 (1062.97) ms
D1 (REST create + query)2087.22 ms2177.16 (2841.78) ms
Turso libSQL (Platform API + database token)3727.39 ms4080.64 (4322.38) ms
Turso engine3792.89 ms4651.5 (4934.59) ms

No create was refused and every tenant answered its first query. D1 was reached over REST; a Worker binding to a new database needs a redeploy, which was not measured. Documented ceilings, not reached here: Turso Free 100 databases11, D1 50,000 databases per account on Paid (raisable on request)3. The schema-per-tenant row shares one project’s resources; the cost and isolation side of that choice is in Where should a tenant live.

A Turso embedded replica is a local SQLite file that syncs from the database12. From the local machine: a replica read p50 0.25 ms (2026-10-15) and 0.31 ms (2026-10-16) against 244.53 and 252.24 ms over HTTP; a write at the primary became visible after one sync() at 913.28 ms p50 (2026-10-15). Opening a fresh replica took 36143.833 ms on 2026-10-15 and 79749.385 ms on 2026-10-16. The same replica pointed at a Turso-engine database did not open (file is not a database); Turso’s docs recommend Turso Sync (@tursodatabase/sync) for new projects13. Its module rows failed on the first account when the Turso organization refused writes (below) and ran in r3 on a second account (n = 30 after 3 warm-up; local read n = 300):

EnginePush p50Pull p50Visible after one pullLocal read p50Same row updated offline on A and B, A pushes first
libSQL531.97 ms301.36 ms30/300.03 msB’s push returned without error and both clients read B’s value, 30 of 30
Turso engine644.36 ms395.07 ms30/300.03 msthe same, 30 of 30

A’s update disappeared with no signal on either engine: the conflict rule is last push wins.

A SQLite-backed Durable Object is one named object with its own SQLite database, reached from the Worker by RPC; the module placed it with locationHint oc, which the docs call “a best effort and not a guarantee”14. Each operation below is one RPC. The r3 battery ran it next to the others; each figure names the Worker colo of its module, and figures from different colos are not compared.

Workload (r3)Durable ObjectD1 in the same runWorker colo
Point read p50 (p25-p75)221 (216-225) ms230 (225-234) msSIN
Job of 8 statements, p50 (p25-p75)1026 (1018-1085) ms1126.5 (1108-1164.75) msKIX
Inserts ops/s at 1 / 4 / 16 / 64 writers5.22 / 20.65 / 74.46 / 224.364.95 / 19.63 / 72.34 / 179.65C1/C4 HKG, C16 HKG+KIX, C64 mixed
100,000-event ingest, summed op time28581 ms29592 msHKG
Window-function aggregate (p95_by_colo) p50426.5 ms484.5 msHKG
Full-text query p50, 4 shapes174-195 ms, top 10 equal to D1’s182.5-200 msHKG
Tenant: burst of 20 to first query, p50 (max)1623.38 (1924.57) ms via the Worker2289.56 (3079) ms via RESTlocal; Worker at SIN on the single-tenant row (TD01k), not recorded for the burst
  • Transactions: sql.exec refused BEGIN TRANSACTION (To execute a transaction, please use the state.storage.transaction() or state.storage.transactionSync() APIs instead of the SQL BEGIN TRANSACTION or SAVEPOINT statements), as the docs say15; a batch run inside transactionSync committed in 238 ms and rolled back as a whole. Read-your-write held 30/30.
  • No vector32 (no such function: vector32), and sqlite_version() is refused, as on D1 (RUNLOG, not in an artifact).
  • In the job-state run (KIX) the job ordering was pg < do ~ turso ~ d1 < postgrest ~ tursodb (also do < d1); in the SIN point-read run it tied D1. The colo, the hour and the fixture differ between those runs.
  • The tenant row is not a like-for-like provisioning comparison: the Durable Object path is one request to the Worker plus three RPCs with no create call, D1’s a REST create and REST queries.
  • Turso answered HTTP 520 on 7 rows in the 2026-10-16 re-run (both engines) and on 4 more in r3 (libSQL only, one in a superseded artifact; none on 2026-10-15). The modules now re-send a lost batch once where a row count or a duplicate key shows whether it landed.
  • The Turso organization (plan_id starter, taken as the Free plan) refused every write during the last module on 2026-10-16 (SQL write operations are forbidden (writes are blocked, do you need to upgrade your plan?)). Read from the Platform API afterwards (not in an artifact), its usage for the billing period showed 11,774,888 rows written against the documented 10 million per month11. Of that, the Turso-engine database counted 9,639,906 and the libSQL one 900,317 over overlapping but not identical workloads.
  • r3 traced that gap on a second account. On one throwaway database per engine and statement shape, inserts, GROUP BY and ORDER BY counted the same on both engines (about 1 row written per row, plus 1 per index entry). The window-function query in the analytics workload, p95_by_colo (a subquery with row_number() and count(*) OVER (PARTITION BY colo)), counted 60,002 rows written on the Turso engine for an ingest of 2,000 rows plus 7 executions, against 4,003 on libSQL for the same ingest and all five dashboard queries: 4 per input row per execution, and none on libSQL. Applied to the first account’s runs this gives 100,000 rows x 23 passes x 4 = 9,200,000 (derived, not re-measured). Whether other window functions or sorts do the same was not measured. With that query skipped on the Turso engine, the full TD05 run at 07:40 (100,000-event ingest plus the dashboard queries, Worker at HKG) counted 200,002 (Turso engine) and 200,028 (libSQL).
  • One Worker colo (SIN), one region (Sydney), one local machine, one run per row unless a row names more. The order between backends held from SIN; from NRT it reversed for full-text search.
  • The Worker-to-Sydney round trip is most of every figure. D1’s own sql_duration_ms for the last_24h aggregate was 0.44 ms of a 225.5 ms operation (2026-10-15).
  • First-query costs are not counted the same way on every path: Postgres connects before the timed operation, Turso, PostgREST and D1 REST pay setup inside the first one. The tables above give the first-query figure wherever it changes the reading.
  • Throughput figures in the contention section are one sample per cell per run; the libSQL drop at 64 writers is the claim that three runs support.
  • In r3 the Worker colo moved between modules (SIN, KIX, HKG) with nothing changed on the client side; r3 figures are compared within one module’s run only.
  • Not compared: cost per month, Auth and row-level security, Storage, Realtime, backup and restore, read replicas (D1 read replication was left at its default), Durable Object point-in-time recovery, and self-hosted runtimes (workerd, celld).
PracticeEvidenceModule
Expect one slow first Turso query per Worker invocation; group a request’s reads.First query p50 729.5-740 ms against 183-189 for the rest of the invocation.TD02, TD05
Send D1 transactions as one batch(); use Turso or Postgres when steps depend on reads.D1 refuses BEGIN TRANSACTION through the binding; batch() rolled back as a whole.TD07
Keep Hyperdrive caching off for anything read right after it is written.29 of 30 reads through the caching config returned the pre-write value.TD07c
Spread writes over databases before 64 concurrent writers hit one Turso libSQL database.24.1 to 30.96 ops/s and 1690.5 to 2592.5 ms per-op p50 at 64 writers in three runs; at 16 the per-op p50 was 234 and 231 ms (2026-10-16). Between 16 and 64 was not measured; spreading over databases was not measured.TD04
Retry BEGIN CONCURRENT transactions on write-write conflict, with a cap.713 conflicts retried at 16 writers on one row; 11 of 64 gave up after 25 attempts.TD04m2
Use FTS5 on D1 or Turso libSQL, not on the Turso engine, for full-text search.The Turso engine refused the FTS5 table on Cloud.TD06
Measure recall at your index arguments before shipping libSQL vector search.recall@10 0.245 and 0.265 with the arguments chosen for load speed; pgvector 1 from SIN (0.95 in the NRT run).TD06
Use a Postgres schema or D1 when a new tenant must answer quickly after signup.Burst p50 to first query from the local machine on 2026-10-16: 1012.95 ms (schema), 2177.16 ms (D1 REST), 4080.64 ms (Turso). A Worker binding to a new D1 database needs a redeploy; not measured.TD01
Measure rows written by window-function queries on the Turso engine first.p95_by_colo counted 4 rows written per input row per execution on the Turso engine, none on libSQL (probe, rows-written-probe.txt); TD05c small run: +300002 against +20029 for the same 10000 events.TD05c; probe script
Detect concurrent offline edits yourself when you sync with Turso Sync.The second push won without an error in 30 of 30 trials on both engines.TD08f
Give each entity its own Durable Object when one Worker coordinates its state.Created on first access, burst p50 to first query 1623.38 ms from the local machine via the Worker; BEGIN TRANSACTION refused, transactionSync batches committed and rolled back (r3).TD01, TD07
Read Turso’s organization usage before and after a load test on a capped plan.TD08 writes refused with SQL write operations are forbidden; the period’s usage, read by hand afterwards and not in an artifact, showed 11,774,888 rows written.TD08
Do transaction steps dependon reads made inside it?Many concurrent writerson one database?nopgvector or row-levelsecurity?yesNeed reads with no network(app or device)?noSupabase Postgres(Hyperdrive, caching off for fresh reads)yesD1yesTurso libSQL(embedded replica)yesSeveral queries per invocation: Turso libSQLOne query per invocation: D1noyesTurso libSQL(interactive transactions)no
  1. If transaction steps depend on reads made inside the transaction, D1 is out (no interactive transactions). Pick Postgres when you also need pgvector (recall 1) or row-level security (not compared here); otherwise Turso.
  2. If many writers hit one database at once, prefer D1 or Postgres, up to the 64 writers measured; Turso libSQL queued at 64 writers.
  3. If the app must read with no network, Turso’s embedded replica is the option here; the D1 binding and the Postgres drivers used in this lab were recorded as having no local replica (not measured).
  4. Otherwise pick on read latency from your colo: from SIN, Turso libSQL, then D1, for invocations that make several queries; for one query per invocation D1 (first query 221-223.5 ms against Turso’s 729.5-740).

Measured claims map to module ids in experiments/sqlite-edge-compare; the run record is its RUNLOG.md and the results by use case its README. Redacted artifacts with their measurement tables are under out/2026-10-15/ and out/2026-10-16/. Every number in a measured row above was taken from those tables; the rows-written probe’s printed output is out/2026-10-16/rows-written-probe.txt (script: scripts/rows-written-probe.ts); the other standalone probes and the hand-read Turso usage named in the text are not in an artifact.

ModuleExperimentTestArtifact
TD01sqlite-edge-comparetd01-db-per-tenant.tsout/2026-10-16
TD02sqlite-edge-comparetd02-point-reads.tsout/2026-10-15, out/2026-10-16
TD03sqlite-edge-comparetd03-job-state-writes.tsout/2026-10-16
TD04, TD04m2sqlite-edge-comparetd04-write-contention.tsout/2026-10-15, out/2026-10-16
TD05sqlite-edge-comparetd05-event-log.tsout/2026-10-16
TD06sqlite-edge-comparetd06-search.tsout/2026-10-15, out/2026-10-16
TD07, TD07csqlite-edge-comparetd07-tx-semantics.tsout/2026-10-15, out/2026-10-16
TD08sqlite-edge-comparetd08-embedded-replica.tsout/2026-10-15, out/2026-10-16
  1. Supabase, “Supabase is acquiring Turso,” Supabase Blog. https://supabase.com/blog/supabase-is-acquiring-turso ↩

  2. Cloudflare, “Limits,” Cloudflare Durable Objects Docs. https://developers.cloudflare.com/durable-objects/platform/limits/ ↩

  3. Cloudflare, “Limits,” Cloudflare D1 Docs. https://developers.cloudflare.com/d1/platform/limits/ ↩ ↩2

  4. Cloudflare, “Debug D1,” Cloudflare D1 Docs. https://developers.cloudflare.com/d1/observability/debug-d1/ ↩

  5. Turso, “Turso Cloud,” Turso Docs. https://docs.turso.tech/turso-cloud.md ↩

  6. Turso, “Concurrent writes,” Turso Docs. https://docs.turso.tech/tursodb/concurrent-writes.md ↩

  7. Turso, “TypeScript SDK reference,” Turso Docs. https://docs.turso.tech/sdk/ts/reference.md ↩

  8. Cloudflare, “D1 Database,” Cloudflare D1 Docs. https://developers.cloudflare.com/d1/worker-api/d1-database/ ↩

  9. Cloudflare, “Query caching,” Cloudflare Hyperdrive Docs. https://developers.cloudflare.com/hyperdrive/concepts/query-caching/ ↩

  10. Turso, “Full-text search functions,” Turso Docs. https://docs.turso.tech/sql-reference/functions/fts.md ↩

  11. Turso, “Pricing,” Turso. https://turso.tech/pricing ↩ ↩2

  12. Turso, “Embedded replicas,” Turso Docs. https://docs.turso.tech/features/embedded-replicas/introduction.md ↩

  13. Turso, “Turso Sync usage,” Turso Docs. https://docs.turso.tech/sync/usage.md ↩

  14. Cloudflare, “Data location,” Cloudflare Durable Objects Docs. https://developers.cloudflare.com/durable-objects/reference/data-location/ ↩

  15. Cloudflare, “SQLite-backed Durable Object Storage,” Cloudflare Durable Objects Docs. https://developers.cloudflare.com/durable-objects/api/sqlite-storage-api/ ↩