Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SQLite 3.45.1 access demo license MIT

# Monday
db = sqlite3.connect("wordbank.db")
db.execute("UPDATE entries SET lookups = lookups + 1 WHERE headword = ?", ("mizzle",))
# ~20 microseconds. No network. No auth. No connection pool.

# Friday, after "we should put this in the cloud"
requests.post(API, json={"namespace": "wordbank", "protocol": "sqlite",
                         "query": "UPDATE entries SET lookups = lookups + 1 ..."})
# ~50 milliseconds. Roughly two thousand times slower, and the transaction
# you were about to open cannot survive to the next line.

free-sqlite-cloud

Hosted SQLite is a real and useful thing. It is also the database category where the gap between what people expect and what they get is widest, because SQLite's defining property — it is a file your process opens — is exactly the property hosting removes.

This repository is about that gap: what actually changes, what to do about it, and the cases where the correct answer is to stop and keep the file. Where a hosted example is needed, it runs against a free hosted instance.

Why anyone wants this

Four reasons, all legitimate:

More than one machine needs the data. A laptop, a CI runner and a small deployed service cannot share a file. This is the honest majority case and the only one where hosting is unambiguously right.

The runtime has no filesystem. Edge functions, serverless handlers and browser-adjacent code often cannot open a file, or get one that vanishes between invocations.

Somebody else has to look at it. Sending a colleague a .db file and hoping they have a client is worse than sending them a URL.

An assistant needs to query it. A model cannot open a file on your laptop. It can call a tool against an endpoint.

Notice what is not on that list: scale. If you are considering hosted SQLite because SQLite is "too small", you want PostgreSQL, and the sooner you accept that the cheaper the migration is.

What breaks when it stops being a file

One writer is still one writer

SQLite serialises writes. WAL mode lets readers continue during a write, which people often mis-remember as "WAL gives you concurrent writes". It does not. Nothing does. There is one write lock and every writer queues for it.

Locally this is nearly free — the lock is held for microseconds and contention is invisible. Hosted, each write holds that lock across a network round trip, and the queue that used to be imperceptible becomes the throughput ceiling for your whole application. The engine behaves identically. The arithmetic does not.

A transaction cannot span two HTTP requests

This is the one that changes your code, not just your latency.

db.execute("BEGIN IMMEDIATE")
row = db.execute("SELECT lookups FROM entries WHERE headword = 'mizzle'").fetchone()
db.execute("UPDATE entries SET lookups = ? WHERE headword = 'mizzle'", (row[0] + 1,))
db.execute("COMMIT")

Locally, correct. Over a stateless HTTP API, three separate requests with no shared transaction context: your BEGIN and your COMMIT may not even reach the same handler, and between the SELECT and the UPDATE anyone can write.

Two ways out. Either send the whole unit of work as one statement so the engine does the atomicity — UPDATE entries SET lookups = lookups + 1 WHERE headword = 'mizzle', which is atomic on its own and is what you should have written anyway — or send a multi-statement script in a single request so it executes as one transaction server-side. What you cannot do is interleave application logic with an open transaction. Read-modify-write across round trips is the bug this category produces most often.

Every query costs a round trip

The performance model inverts. Locally, the cost of a query is the work it does, and the number of queries barely matters. Hosted, the cost is dominated by the round trip, and the number of queries is the only thing that matters.

That turns a familiar non-problem into a serious one. A loop that fetches a row per iteration is a mild inefficiency against a local file and a disaster against an HTTP endpoint — a hundred iterations is a hundred round trips. JOIN, IN (...), and returning one result set instead of a hundred stop being style preferences and become the difference between 50ms and 5 seconds.

The rule that follows: count round trips, not queries. If you can express something as one statement, do, even when the single statement is uglier.

Watching it happen

concurrency_demo.py runs N concurrent connections incrementing one counter and reports what SQLite does about it. No credentials, no network — it uses a temporary local file, because the lock semantics are a property of the engine and hosting only adds latency on top.

python3 concurrency_demo.py                     # 8 writers, WAL, 5s busy_timeout
python3 concurrency_demo.py --busy-timeout 0    # no patience: watch retries appear
python3 concurrency_demo.py --journal delete    # the pre-WAL rollback journal
python3 concurrency_demo.py --compare           # all four combinations, one table

--compare from one laptop, running SQLite 3.45.1:

journal    timeout   committed   final  retries   commits/s
-----------------------------------------------------------
wal           5000         320     320        0       11196
wal              0         320     320       17       13723
delete        5000         320     320        0         893
delete           0         320     320       88        1270

These are one machine's numbers, not a benchmark of anything — run it yourself and you will get different ones. The shape is what matters, and there are three things in it:

  1. final always equals committed. SQLite did not lose an increment or corrupt a row under any configuration. Correctness was never the question.
  2. WAL is dramatically faster than the rollback journal under contention, which is why it is the default worth setting. It still did not let two writers write at once — it let readers not block.
  3. Dropping busy_timeout to zero produced retries, not errors, only because the demo retries in a loop. A client that does not retry gets SQLITE_BUSY and, if it swallows that, silently loses the write. That is the failure mode: not a crash, a number that is quietly wrong.

Raise --writers far enough with --busy-timeout 0 and you will see the ABANDONED line appear. Every abandoned update is data your application believed it had stored.

Three shapes of hosted SQLite

HTTP query API Embedded replica (libSQL / Turso) Ship the file
Read latency One round trip per query Local — reads hit a file on disk Local
Write latency One round trip One round trip to the primary Local
Multiple machines Yes Yes No
Multi-statement transactions Only as one batched request Yes, against the replica's local copy Yes
Offline No Reads work; writes queue or fail Yes
Setup cost A URL A client library and a sync story Nothing

The HTTP query API is the simplest thing that can work, and the one this repository's examples use. Every statement is a request; nothing is installed; anything that can make an HTTPS call is a client, including an AI assistant. The cost is the round trip and the loss of client-held transactions.

Embedded replicas are the more sophisticated answer, and Turso is the reference implementation. You keep a local copy of the database that your process reads at file speed, and writes go to the primary and replicate back. For read-heavy workloads this is strictly better than an HTTP API — reads stop costing anything at all. Turso has invested more in hosted SQLite than anyone else: the libSQL fork, embedded replicas, and a from-scratch rewrite of the engine in Rust. If your workload is read-dominated and you can adopt a client library, look there first.

The trade is that you have adopted a library, a sync model, and a local file that can be stale. The HTTP approach has no client to install and no consistency question — it is slower and simpler in exactly the places the replica approach is faster and more complicated.

Shipping the file is not a joke option. For reference data that changes on a deploy cadence — a lookup table, a documentation index, a pricing catalogue — baking a read-only .db into your container image gives you zero-latency reads, no service to be down, and no bill. If nothing writes at runtime, hosting buys you nothing.

When local SQLite is simply the right answer

Say it plainly, because a repository about hosted SQLite has an obvious incentive not to.

  • One process writes, and it is the only one. A desktop app, a CLI tool, a single-instance service with a persistent volume. Hosting adds latency and a dependency and removes nothing.
  • The data is read-only at runtime. Ship it. A file in a container image cannot have an outage.
  • It is a test fixture. :memory: or a temp file per test. A shared hosted database is the enemy of test isolation and you will spend more time on cleanup than the tests take to run.
  • You are teaching SQL. A file and the sqlite3 binary have no failure modes involving networks, tokens or somebody's corporate proxy.
  • Latency matters more than sharing. If a request path does twenty queries and you cannot reduce it, twenty round trips will cost more than every optimisation you have made this year saved.

If none of those describe you, hosting is reasonable. If one of them does, the honest advice is to keep the file and revisit this when the constraint changes.

Getting a free instance

freebase.cloud runs SQLite 3.45.1 behind an HTTP query API and an MCP endpoint. No credit card. Free tier, suited to development, prototyping and small production workloads — which for SQLite is a wider band than it sounds, because SQLite's workloads are usually small by construction.

Be clear about what you get: there is no raw socket and no file handle. You send statements and receive results.

curl -X POST https://freebase.cloud/api/wire/query \
  -H "Content-Type: application/json" \
  -d '{"namespace":"wordbank","protocol":"sqlite","query":"SELECT sqlite_version()"}'

For assistants, take a token from Settings → MCP → New Token and register the endpoint. The connection name prefixes the tools; this repository uses wordbank.

claude mcp add --transport http wordbank https://freebase.cloud/api/mcp/YOUR_TOKEN
Tool Use
wordbank_query SELECT, including CTEs, window functions and FTS5 MATCH
wordbank_store INSERT / UPDATE / DELETE
wordbank_list_tables What exists, without the model guessing
wordbank_annotate_table Describe a table once so later sessions do not re-derive it

SQLite connections additionally expose sqlite_master and sqlite_version helpers, which is the fastest way to get an assistant to read the real schema instead of inventing a plausible one.

Dialect notes that catch people

  • Type affinity is not type checking. A TEXT column will accept an integer. If you want the constraint, write it: CHECK (typeof(region) = 'text').
  • AUTOINCREMENT is usually wrong. INTEGER PRIMARY KEY already aliases the rowid and reuses gaps; AUTOINCREMENT only adds a monotonicity guarantee and a second table to maintain.
  • FTS5 is excellent and separate. CREATE VIRTUAL TABLE ... USING fts5(...) gives you real ranking via bm25(). Use an external-content table plus triggers so the text is not stored twice — examples/wordbank.sql does exactly that, and the same virtual table works on the hosted build.
  • Dates are text, numbers or nothing. There is no date type. Store ISO-8601 strings, use datetime(), and be consistent — the sort order of a well-formed ISO string is the sort order of the date, which is why the convention works.
  • PRAGMA foreign_keys defaults to off in many drivers. Your foreign keys are decorative until you turn it on.

What is in here

File What it does
concurrency_demo.py Concurrent writers against a local file; prints contention, retries and lost updates
examples/wordbank.sql A real schema: entries, citations, FTS5 with external content, triggers, useful queries
examples/http_query.sh The HTTP query API end to end, including the round-trip counting lesson
examples/README.md How to run both, locally and hosted

Reference

Third-party details last verified: 2026-08-18.


freebase.cloud is an independent service and is not affiliated with the SQLite project, Hipp, Wyrick & Company, Inc., Anthropic, PBC, or Turso.

About

Free SQLite in the cloud — hosted SQLite you can query over HTTP without shipping a file around

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages