The short answer
Choose SQLite when one app owns its data and that data fits on one server. Choose Postgres when many writers or many services share one dataset, or when you need its extensions and types.
They aren’t rivals for the same job. In SQLite’s own words, it “does not compete with client/server databases”.
We have a stake in this: Eurobase offers SQLite databases for Workers and doesn’t offer managed Postgres. So this article also says where our databases are the wrong choice.
Decide in 30 seconds
| Your app | Pick |
|---|---|
| One app owns its data | SQLite |
| Each project keeps its data apart | SQLite |
| Mostly reads, with moderate writes | SQLite |
| A coding agent builds and deploys the app | SQLite |
| Many writers on the same data, all the time | Postgres |
| Several services share one database | Postgres |
| One dataset of hundreds of gigabytes or more | Postgres |
| Extensions such as PostGIS or pgvector | Postgres |
| Arrays, ranges or timestamps with time zones | Postgres |
| An app that already runs on Postgres | Postgres |
If your answers point both ways, the Postgres rows usually decide: they’re the hard ones to work around. On Eurobase, a database stops at 2 GB on paid plans, so a larger dataset needs a home elsewhere.
Where each one runs
Postgres is a server. Your app usually connects to it over the network, and it starts a new process for each connection. The default limit is typically 100 connections. That’s why many apps put a connection pool in front of it.
SQLite is a library that reads and writes a file. It has no server process and nothing to connect to.
Eurobase changes one thing. Each database gets a small process of its own on the server that runs your app, and your app reaches it through a local socket. Your app never touches the file, and the query never crosses a network.
When SQLite is the better choice
- Each project keeps its data apart
- Every database has its own file, process and memory limit. A slow query stalls only that database, and there’s no shared server to size. On Eurobase, a database belongs to one project, which can have up to 3 on Free and 10 on paid plans. For a database per customer, deploy a project per customer.
- No connection pool
- Your code calls
env.DB. There’s no connection string to store, no password to rotate and no pool to tune. - Queries run next to the app
- With no network in the path, a query has no network delay. On a test server with 3 vCPUs and 4 GB of memory, 100 databases took 1,936 calls a second in total, from clients on that server, with no Worker in the path. p99 was 4 ms for reads and 6 ms for writes, and the slowest write took 150 ms.
- Backups you don’t run
- Every committed change is copied continuously to object storage on European infrastructure, where each copy is locked against deletion. There’s no backup job to set up or check.
- Small and medium apps
- SQLite says it works for most low to medium traffic websites, and calls 100,000 hits a day a conservative estimate, not an upper bound. In WAL mode, readers don’t block the writer, and the writer doesn’t block readers. On Eurobase, each database runs one call at a time, so a long query delays that database’s other calls.
- Apps built by coding agents
- The database’s setup lives in the repository: a few lines of config and SQL files. An agent needs no second account and no database credential, and it can run the same migrations locally before it pushes.
When Postgres is the better choice
- Many writers on one dataset
- SQLite allows one writer at a time, per database. In Postgres, row locks block only writers of the same row, so transactions on different rows run side by side. If your app writes all day from many clients to the same tables, this is the reason to choose Postgres.
- Very large datasets
- The SQLite format reaches 281 TB, far beyond the size of a Eurobase database. Postgres has no limit on database size and allows 32 TB per table, and it can add standby servers that answer read-only queries.
- Extensions
CREATE EXTENSIONadds features to the database itself: PostGIS for geospatial data, pgvector for vector search.- Rich types
- SQLite has five storage classes and no date or boolean type. Postgres has many built-in types, including
jsonb,uuid,timestamptz, arrays and ranges. ItsALTER TABLEalso does far more than SQLite’s limited subset. - Many services on one database
- When several services reach the same data over a network, SQLite itself says to use a client/server database. Postgres can also give each service its own role, and row security policies limit what each one sees. On Eurobase, apps in other projects can’t reach your project’s database.
- Recovery in your own hands
- With Postgres you can run
pg_dumpat any time, and, with WAL archiving set up in advance, restore to a point in time. On Eurobase, you can’t export a database or restore it to an earlier point yourself. - An app that already runs on Postgres
- Types, SQL and drivers all differ, so moving to SQLite means changing your data layer. If your app works, keep Postgres.
What to know about SQLite on Eurobase
- Databases work with Workers. A project that runs as a container can’t use them.
- You can’t use
BEGINin your SQL. To run several statements in one transaction, send them withenv.DB.batch(). Your code can’t hold a transaction open between statements. - A write is confirmed once it’s committed on the server, and its upload starts at the same moment. If a server crashes or is lost, the confirmed writes not yet uploaded are lost with it: usually the last fraction of a second, more while the backup is behind.
If you need Postgres
You can host your app on Eurobase and keep your database with a Postgres provider. A container can connect with any Postgres driver to a host with an IPv4 address. A Worker can’t open a Postgres connection, so it needs the provider’s HTTP API. Eurobase + Supabase shows both.
Add SQLite to a Worker on Eurobase
Declare the database in your Wrangler config:
// wrangler.jsonc
{
"d1_databases": [
{ "binding": "DB", "database_name": "shop" }
]
}Add a migration:
-- migrations/0001_init.sql
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT, price REAL);Query it through env.DB with the D1 API:
// src/index.ts
export default {
async fetch(request, env) {
const { results } = await env.DB
.prepare('SELECT id, name FROM products WHERE price < ?')
.bind(20)
.all();
return Response.json(results);
}
};Push. Eurobase creates the database with the next deployment and runs new migrations before that deployment goes live. If a migration fails, the deployment fails and the previous one keeps serving. Migrations that already ran stay applied, and a rollback doesn’t undo them. Variables and bindings has the details.
Limits
| Limit | Free | Paid plans |
|---|---|---|
| Databases per project | 3 | 10 |
| Size of a database | 100 MB | 2 GB |
| Rows read per day | 5,000,000 | No daily limit |
| Rows written per day | 100,000 | No daily limit |
| Data written per day | 400 MiB | 2 GiB |
| Time per query or batch | 30 s | 30 s |
| Backup history | 7 days | 30 days |
Daily limits apply to each database and reset at 00:00 UTC. Over a row limit, queries fail until the reset. Over the data limit, reads keep working and writes fail. Data written counts changed database pages, at least one per write, so small writes use it faster than their size suggests. A deployment that would add more databases than your plan allows is refused.



