When SQLite makes sense, how to migrate from PostgreSQL or MySQL, and what trade-offs you're actually making.

You've been running PostgreSQL in production for three years. Your database has 50GB of data, handles 10,000 queries per minute, and costs you $200/month on a managed service. Your application works fine, but you're tired of connection pool tuning, read replica lag, and paying for compute you barely use.
Last week, you saw another developer deploy their entire SaaS on SQLite with Turso. They're handling similar traffic, paying about $25/month, and sleeping better at night. You're wondering: should I migrate?
The traditional answer has always been "no." Database migration is risky, SQLite can't handle production workloads, and you need PostgreSQL's features. But over the past few years, something shifted, and the old advice no longer holds the way it used to.
This guide will help you understand when SQLite makes sense, how to migrate from PostgreSQL or MySQL, and what trade-offs you're actually making.
For years, the standard advice was simple: start with PostgreSQL. SQLite is fine for development, but production needs a "real" database. Frameworks defaulted to PostgreSQL, hosting platforms pushed managed Postgres instances, and the pattern became cargo cult: everyone used PostgreSQL because everyone else did.
But several things changed.
SSDs became standard. SQLite's write performance was historically limited by disk I/O. Modern NVMe SSDs handle hundreds of thousands of IOPS, and the bottleneck largely disappeared.
Better defaults became common knowledge. Out of the box, SQLite ships conservative settings tuned for an older era of hardware, and WAL journaling is opt-in rather than the default. Setting journal_mode=WAL, increasing cache_size, and tuning mmap_size yields dramatically better performance. Aaron Francis has documented this extensively in his High Performance SQLite course. Turso will also ship with many such defaults out of the box!
The database moved into the application. Deploying PostgreSQL close to users in many regions is expensive and complex. A SQLite database is a file that can live inside your application process, sync to devices, and run wherever your code runs. Turso built its Cloud platform on this principle, offering the choice between an HTTP-based protocol for remote connectivity, or local databases that bidirectionally sync to the cloud.
Operational complexity has a cost. Connection pooling, replication lag, and failover complexity are problems you have to solve when you run PostgreSQL. SQLite eliminates entire categories of them.
The result: SQLite now carries serious production workloads. Expensify built its backend on SQLite through its Bedrock server and has processed enormous transaction volumes on it. Fly.io has published extensive advocacy for server-side SQLite and built LiteFS for replicating it. And Rails 8 shipped with SQLite as a production-capable default, backed by SQLite-based Solid Queue, Solid Cache, and Solid Cable, with DHH publicly championing the change. On the Turso Cloud itself, thousands of new developers deploy their applications every week.
SQLite is a better choice than PostgreSQL for many applications, though far from all of them.
You have a read-heavy workload. With reads served from a local file, there's no network overhead and no connection pooling, and query planning is simpler. Even for remote access on the Turso Cloud, its HTTP-based protocol guarantees that establishing connections is much cheaper. For workloads with occasional writes and frequent reads, SQLite excels.
You want operational simplicity. There's no separate database server to manage, and your database is a file that travels with your application. A backup is a file copy, and a restore is the same. This simplicity has its own benefits.
You're building close to your users. With Turso, your application can keep a local database and sync with Turso Cloud, so reads happen at local speed on the server, on a device, or in the browser. This architectural pattern is a poor fit for a traditional client-server PostgreSQL setup.
You care about cost at moderate scale. A managed PostgreSQL instance for a moderate production workload commonly runs into the hundreds of dollars per month. Turso's paid plans start with the Developer plan at $4.99/month, and the Scaler plan is $24.92/month when paid yearly.
You need true multi-tenancy. Giving each tenant their own database is prohibitively expensive with PostgreSQL. With Turso, databases are lightweight and cheap to create: the free tier includes 100 databases, and every paid plan includes unlimited databases. This enables clean isolation without complex row-level security. Those use cases are just a lot more common now that agents write the code: there are more independent applications being generated, and agents themselves are applications with their own state.
Migrating from PostgreSQL or MySQL to SQLite requires translating schemas, converting data, and testing thoroughly. Here's the step-by-step process.
Export your schema and data separately, and use INSERT statements for the data rather than the default COPY format, since SQLite can't consume COPY:
# Export schema only
pg_dump -h your-host -U your-user -d your-database --schema-only > schema.sql
# Export data only, as INSERT statements
pg_dump -h your-host -U your-user -d your-database --data-only --inserts > data.sql
Expect to hand-edit both files. PostgreSQL dumps include statements SQLite doesn't understand (SET commands, ownership, sequences), which need to be stripped before import.
For MySQL, use mysqldump:
# Export schema
mysqldump -h your-host -u your-user -p --no-data your-database > schema.sql
# Export data
mysqldump -h your-host -u your-user -p --no-create-info --skip-extended-insert your-database > data.sql
The --skip-extended-insert flag produces one INSERT per row, which is easier to clean up for SQLite. Community-maintained mysql-to-sqlite conversion scripts can automate most of the remaining syntax fixes.
PostgreSQL and MySQL use syntax that doesn't directly translate to SQLite. Common translations:
| PostgreSQL/MySQL | SQLite | Notes |
|---|---|---|
SERIAL | INTEGER PRIMARY KEY | Auto-increments by rowid behavior |
BIGSERIAL | INTEGER PRIMARY KEY | SQLite integers are 64-bit |
BOOLEAN | INTEGER | Stored as 0 or 1; SQLite also accepts TRUE and FALSE keywords as literals |
TEXT[] | TEXT | Store as a JSON array and use SQLite's JSON functions. Turso has experimental support for arrays. |
NOW() | datetime('now') or CURRENT_TIMESTAMP | Both work in SQLite |
AUTO_INCREMENT | (usually nothing) | INTEGER PRIMARY KEY auto-increments on its own; the AUTOINCREMENT keyword is rarely needed and adds overhead |
Example schema translation:
-- PostgreSQL schema
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
active BOOLEAN DEFAULT true,
tags TEXT[],
created_at TIMESTAMP DEFAULT NOW()
);
-- SQLite equivalent
CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
active INTEGER DEFAULT 1,
tags TEXT, -- store as JSON
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
Turso requires your SQLite database to be in WAL mode before importing, with all changes checkpointed into the main database file:
# Convert to WAL mode and checkpoint using the TursoDB command line
tursodb your-database.db "PRAGMA journal_mode=WAL;"
tursodb your-database.db "PRAGMA wal_checkpoint(truncate);"
# Import to Turso, using the Turso Cloud CLI
turso db import your-database.db
The import command creates a database named after the file and brings in all your tables, data, and schema automatically. You can also pass a group with --group, or use the older turso db create your-database --from-file your-database.db form.
For programmatic migrations, the Platform API flow has three steps: create the database with a seed of type database_upload, generate a token for the new database, then upload the file directly to the database:
# 1. Create the database, declaring an upload will follow
curl -X POST "https://api.turso.tech/v1/organizations/{org}/databases" \
-H "Authorization: Bearer $TURSO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "name": "your-database", "group": "default", "seed": { "type": "database_upload" } }'
# 2. Create a token for the new database
curl -X POST "https://api.turso.tech/v1/organizations/{org}/databases/your-database/auth/tokens" \
-H "Authorization: Bearer $TURSO_API_TOKEN"
# 3. Upload the file directly to the database, using the database token from step 2
curl -X POST "https://your-database-{org}.turso.io/v1/upload" \
-H "Authorization: Bearer $DATABASE_TOKEN" \
--data-binary @your-database.db
Uploads of up to 20GB are supported.
Before switching production traffic:
Run your test suite. Good test coverage catches most schema issues immediately.
Compare query plans. Use EXPLAIN QUERY PLAN in SQLite and EXPLAIN in PostgreSQL to verify that queries use indexes appropriately.
Load test. Use a tool like k6 or Apache Bench to simulate production traffic patterns, and measure latency at p50, p95, and p99.
Test concurrent writes. SQLite uses write-ahead logging to allow concurrent reads during writes, but only one write proceeds at a time. Verify that your write patterns work with this model. Worth noting: Turso's engine adds BEGIN CONCURRENT for improved write throughput using MVCC, and concurrent writes in Turso Cloud are in early access. If you are using Concurrent Writes on the Turso Cloud, the journal mode will be mvcc, and not wal.
Simplicity. Your database is a file, without connection strings, a separate server, or connection pools. Your mental model collapses to "it's just data stored on disk."
Cost savings. For a moderate workload, the difference between a managed PostgreSQL instance and a Turso plan commonly runs to thousands of dollars a year.
Local reads everywhere. With Turso Sync, your application keeps a local copy of the database and syncs with Turso Cloud, so reads come from local disk on your servers and even on user devices. This pattern is architecturally out of reach for traditional client-server PostgreSQL.
Operational peace. No more worrying about connection pool exhaustion, replication lag monitoring, or failover complexity. Failure modes are simpler.
Complex JOIN performance. PostgreSQL's query optimizer handles multi-table JOINs with sophisticated planning, while SQLite's optimizer is simpler. Applications that rely heavily on JOINs across many tables with complex WHERE clauses may see slower query performance.
Triggers and Stored procedures. SQLite supports triggers, but they're more limited than PostgreSQL's, and there are no stored procedures. Business logic that lives in stored procedures needs to move to application code.
Advanced data types. Turso in fact has experimental support for User-Defined Types, including arrays, but it is not yet available in the Cloud, and it is simpler than Postgres with types coming from its rich extension ecosystem (like PostGIS). Applications that depend on PostgreSQL-specific types face a more complex migration. Note that full-text search is available in SQLite through FTS5, so search alone is rarely a reason to stay.
Built-in replication. PostgreSQL has native replication and SQLite doesn't. Turso addresses this at the platform level with sync between local databases and Turso Cloud, which for many applications is simpler than managing PostgreSQL replicas yourself.
You should consider migrating if:
Stay on PostgreSQL if:
If you decide to migrate:
Start with a non-critical database. Migrate a secondary service or a new feature first rather than your main production database.
Use Turso's free tier. 100 databases and 5GB of total storage, no credit card required. That's enough to fully test a migration before committing.
Read Turso's migration guide at docs.turso.tech/cloud/migrate-to-turso, which covers both the CLI and Platform API paths.
Join the Turso Discord. The community includes many developers who've migrated from PostgreSQL and can help troubleshoot issues.
Migration isn't right for every application. But if you're running PostgreSQL mostly out of habit, it's worth questioning that choice. SQLite today is fundamentally different from SQLite a decade ago. The tooling has matured, the frameworks have come around, and Turso has solved the operational problems that once made SQLite impractical at scale.
The simplest database is often the best database.