yapern

Create a message

B I S x2 H “” </> •••

0/2000

Create new sub-yap

← Blog

Turso vs Layerbase Free Plan sqlite Systems

· Updated · Yapern

The first version of this test left Layerbase rejecting even SELECT 1 more than five minutes after the write load stopped. Layerbase said it had traced the stall to a write-queue bug in the open-source libSQL server, sqld, and shipped a fix, then asked for another run. I reran Layerbase on October 20. The Turso results below are unchanged from the original test.

This time, Layerbase finished all six workloads and verified every acknowledged write. It sustained 117 individual inserts per second at 128 concurrent writers in a twenty-second confirmation, compared with Turso’s 55. Turso still led reads by a wide margin. And the retest found limits worth keeping in view: connection resets under a sudden burst and database-lock errors with larger batches.

I have no affiliation with Turso or Layerbase. Both databases were on their providers’ Free plans.

The same rows, the same HTTP requests

Each database originally started empty. The seed was 10,000 users, 2,000 projects, and 100,000 events, with primary keys, foreign keys, unique emails, and indexes for finding recent events by user or project. A fourth table collected benchmark inserts. Every inserted payload contained 256 ASCII bytes. The Layerbase retest reused these tables, checked the seed counts, and retained the earlier benchmark writes.

The client was Node.js 22 on the same macOS machine. It reused TCP/TLS connections through Node’s https.Agent with keepAlive: true and sent one SQL statement through the Hrana HTTP pipeline per request, followed by a stream close. Layerbase’s platform-side pooling setting wasn’t recorded in either test. This measures a hosted libSQL-compatible API, including the network and request handling. It doesn’t measure SQLite opening a local file.

Each concurrency level got a two-second warmup and an eight-second measurement. The best clean level then got a twenty-second confirmation. Workers waited for a response before sending another request; failed requests weren’t retried. In-flight work was allowed to finish, and that drain time counted in the throughput denominator. HTTP errors, SQL errors, and incorrect returned rows all counted as failures. The request timeout remained fifteen seconds.

The runner and schema came from the original benchmark repository at commit 6f0aacc, without changing the measured workloads. The logs still record SQLite 3.45.1 on Layerbase and 3.47.0 on Turso. Server regions, CPU allocations, and storage settings weren’t controlled, so this compares the hosted offerings as tested across the two sessions.

Twenty-second confirmations with zero errors, including warmup
WorkloadTurso, originalLayerbase, retest
Point lookup, one user2,266 queries/s
192 concurrent
74 queries/s
32 concurrent
Indexed join, 20 result rows1,492 queries/s
128 concurrent
128 queries/s
128 concurrent
Single-row insert55 rows/s
64 concurrent
117 rows/s
128 concurrent
Atomic counter update60 updates/s
32 concurrent
119 updates/s
128 concurrent
80% reads, 20% inserts278 operations/s
32 concurrent
109 operations/s
32 concurrent
100-row insert batch4,169 rows/s
16 concurrent
2,527 rows/s
4 concurrent

The original Layerbase run confirmed 113 point reads and 133 joins per second; its 125 inserts per second came from an eight-second stage before the stall. The retest gives that insert result a longer confirmation, but it doesn’t close the read gap. At a matched 64 concurrent requests in the eight-second ramps, Turso delivered 793 point reads per second and Layerbase delivered 79.

Adding clients helped reads much more than writes

Turso’s read curve kept climbing as more requests were allowed in flight. Its insert curve flattened around 50 to 55 requests per second. Going from 32 to 128 concurrent inserts mostly bought a longer wait, with p95 latency rising from about 715 milliseconds to 2.8 seconds in the short stages. Layerbase’s retest reached 142 inserts per second in the eight-second stage at 128 workers, then 117 in the longer confirmation.

Clean eight-second ramps compare the original Turso run with the Layerbase retest. Turso point reads rise to 1,506 requests per second at 128 workers; Layerbase peaks at 85 at 32. Layerbase single-row inserts reach 142 at 128 workers, while Turso stays near 50. Layerbase’s failing 128-reader warmup is omitted.

The missing Layerbase read point at 128 isn’t a zero-throughput measurement. Its warmup produced 40 connection resets in 147 attempts, so the runner skipped the measured stage. The 128-writer ramp completed without errors. Turso’s later 192-concurrent read check isn’t plotted here because it came from a separate original follow-up run.

There’s a familiar SQLite constraint in the background: ordinary SQLite serializes writes even when WAL lets readers proceed alongside them. That doesn’t identify the bottleneck in either hosted service. These logs can’t separate writer contention, commit handling, storage, or service admission control. They do show when increasing concurrency stopped improving this client’s write rate.

A hundred rows changes the job

For an import, I don’t particularly care how many HTTP responses arrive per second. I care how many records have been saved. The batched test kept the same payload, foreign key, and indexes, but put one hundred rows into a single atomic INSERT.

Turso handled about 42 of those requests per second, down from 55 single-row requests. Each successful batch saved a hundred rows, producing 4,169 inserted rows per second. Calling that “4,169 writes per second” without the batch size would be misleading. It would sound like four thousand independently acknowledged transactions.

Turso completes 55.09 single-row insert requests per second versus 41.69 requests with one hundred rows per batch. The inserted-row rate rises from 55.09 to 4,168.66 rows per second. Both are clean twenty-second confirmations from the original test.

Layerbase’s retest confirmed about 25 batch requests per second, or 2,527 rows per second, at four workers. At sixteen workers, 29 of 499 measured requests returned SQLITE_BUSY with “database is locked,” about 5.8%. The runner stopped escalating that workload and confirmed the clean four-worker level. The faster stage with errors isn’t used in the comparison table.

The other completed Turso workloads fit its write-rate picture. Atomic counter updates sustained about 60 per second. An 80% read, 20% insert mix sustained 278 operations per second, including roughly 56 inserts per second. Layerbase confirmed 119 counter updates and 109 mixed operations per second; the latter included about 22 inserts per second.

After the full Layerbase retest, all 150,850 acknowledged new rows were present, and the counter sum matched all 7,139 acknowledged updates. The original Turso run verified 219,157 acknowledged inserts and 4,010 updates. Both passed payload-length checks, foreign-key checks, and quick_check. These checks establish that acknowledged test writes were present afterward; they don’t simulate losing the server or its storage.

The original stall

In the original Layerbase test, the 128-concurrent insert stage produced 128 client timeouts at the fifteen-second deadline. Follow-up requests began returning HTTP 429 with this response:

{"error":"Timed out while opening database connection"}

The load stopped around 04:07:37 UTC on October 20. Ten later health probes, each with a fresh client and at most one connection, failed from 04:09:29 through 04:12:42 UTC. Even SELECT 1 was still failing more than five minutes after the load stopped. That prevented final write verification and the remaining workloads. At the end of that run, 297 unacknowledged insert attempts had uncertain outcomes; a timeout doesn’t prove a write wasn’t committed.

What changed after Layerbase’s fix

In its response supplied for this retest, Layerbase attributed the stall to a timing bug in sqld’s write queue. A request that had already given up could leave the write lock stuck, stopping later writes until a restart. Layerbase said it reproduced the bug in the latest upstream release and deployed a periodic checkpoint that safely clears the queue. It said migration work may have affected speeds, but attributed the severe failure to the upstream bug.

Layerbase reported 45 internal runs with up to 128 writers, zero stalls, and zero lost writes, and said the fix was live on every Free account. Those are the provider’s results. No patch or server build ID was supplied, so this retest checks the endpoint’s behavior without independently auditing that diagnosis or implementation.

The gradual ramp now reached 128 writers and completed its twenty-second confirmation with zero errors. The full suite then finished, including updates, mixed traffic, batch inserts, and the integrity checks that the old stall had prevented.

A separate attempt to start directly at 128 writers from a fresh client was rougher. Its warmup completed 51 inserts and hit 96 ECONNRESET failures in 147 attempts. The runner stopped that stage before the timed measurement, so the planned twenty-second stage and sixty-second confirmation supplied no throughput result. All 51 acknowledged rows were present in the immediate verification. This failed burst is retained in the downloadable reports.

The first of ten fresh-client health probes after that burst also returned ECONNRESET. The next nine passed, from about 35 through 315 seconds after the run finished. Each used a new client with one socket and a five-second timeout. A final single-row insert succeeded, and another fresh client read it back and confirmed all 51 acknowledged burst inserts plus that diagnostic row. The original fifteen-second timeout followed by prolonged HTTP 429 responses did not recur in this retest.

Layerbase’s published Free plan lists 20 concurrent connections. This client went well beyond that allowance. HTTP requests, Hrana streams, and the platform’s connection accounting aren’t interchangeable, and the logs don’t show how that cap was applied. These are observations about deliberate overload, not evidence that the plan promises 128 simultaneous writers.

Turso also pushed back in the original tests. At 256 concurrent reads, the short stage returned 257 HTTP 429s out of 23,982 attempts, about 1.1%. A longer check at 224 had 198 out of 52,915, about 0.37%. Its response identified load shedding; the database hostname is redacted here:

{"error":"load-shed for host [database-host]"}

At 192 concurrent reads, Turso confirmed 2,266 successful queries per second with no errors. Health and integrity queries succeeded after each original run. Its 128-concurrent insert stage also completed without errors, though at about 51 rows per second.

I wouldn’t turn these episodes into uptime statistics. There was one tested endpoint per provider and no server-side trace. Layerbase’s completed write ramp, its failed fresh-client burst, and its batch-lock errors all belong in the result.

What the free plans actually include

The public offers, rechecked on October 20, 2026, put different limits around a small application. The monthly row allowances describe how much work is included. They don’t tell you how quickly a burst will finish.

Published Free-plan terms
IncludedTurso FreeLayerbase Free
Databases1002
Storage5 GB5 GB
Monthly rows read500 millionNo row meter advertised
Monthly rows written10 millionNo row meter advertised
Concurrent connectionsNot listed on the pricing page20

Layerbase also advertises sleep after fifteen idle minutes and a 10 GB per 24 hours acceptable-use allowance on Free-plan bandwidth. “No row meter” doesn’t mean infinite throughput. On Turso, rows read can include scans beyond the rows returned. Our count of successful HTTP requests, and even our count of saved application rows, shouldn’t be substituted for a billing usage report.

Enough evidence to repeat the test

The benchmark bundle includes the schema, runnable clients, plotting script, original reports, and new Layerbase reports with database hostnames removed. It keeps the failed stages as well as the clean ones. The current runner stops remaining load on a timeout or HTTP 429; that circuit breaker was added after the first Layerbase run, which was stopped manually.

For Layerbase, set LIBSQL_URL and LIBSQL_AUTH_TOKEN in a local .env, then run the downloaded client from its containing directory. The retest used --reuse to retain the original benchmark tables; omit that flag on a fresh database:

node run.mjs --provider=layerbase --reuse --max-concurrency=128

The separate burst attempt added --concurrency=128 --families=single_insert --duration=20 --confirmation=60. Its failed warmup prevented both measurements. The included post-load.mjs runs fresh-client health probes and a final diagnostic write; its report records actual probe times. The README identifies the reports used for each figure and table. Turso’s original reports and batching figures remain unchanged.

These are short, warmed stages on a small dataset. They don’t establish sustained production capacity, cold-start behavior, or performance on a multi-gigabyte database. Reusing the Layerbase tables also means its retest began with earlier benchmark writes still present.

Layerbase now has a complete, verified workload test, so the stalled endpoint shouldn’t be the last word on it. For an app like Yapern, I’d still test opening threads and saving replies with a burst of arriving clients included. The clean write ramp and the fresh-client burst gave different answers, and a peak throughput number would hide that difference.