Almost certainly later than you think. SQLite handles more SaaS workload than founders realize — and Postgres has a real operational tax. Migrate when the symptoms are concrete, not when the Hacker News thread tells you to.
Methodology. Numerical thresholds reference SQLite’s “When to use” documentation and PostgreSQL’s official docs. We currently run two production apps on SQLite (with WAL mode) and three on Postgres. How we research.
Pieter Levels has discussed running multi-million-dollar SaaS products on a single VPS with simple stacks for years. Tailscale shipped its first production database on SQLite-via-etcd, then later on a single Postgres node, and the engineering write-ups suggest both choices were correct at the time. The point isn’t that SQLite is always right. It’s that the default assumption — “real apps need Postgres” — gets a lot of solo founders to over-engineer too early.
SQLite’s official documentation gives a usable rule of thumb: SQLite handles applications with up to ~100k page views per day on cheap hardware, comfortably. Per the SQLite team, the database is “the most widely deployed and used database engine” on Earth. It is not a toy.
Before you migrate, audit honestly: are you actually hitting a SQLite limitation, or are you migrating because the internet told you to? SQLite is a strong default for any app that fits all of these conditions:
cp app.db backup-$(date).db while WAL is on. That’s the disaster-recovery story for early-stage SaaS.If all five describe your app, stay on SQLite. Don’t migrate. The operational cost of running Postgres — even managed Postgres — is non-zero, and the latency cost of moving the database off the application server is always positive.
Here’s the inverse: signals that the SQLite default has stopped being the right call. Numbers are approximate; treat them as orders of magnitude rather than exact cutoffs.
SQLite serializes writes. At low write rates this is invisible. Past ~100/sec sustained, write contention shows up as lock-wait latency. If your monitoring shows writes blocking on each other, Postgres’s MVCC concurrency model is the right answer.
SQLite is a single-machine database. Tools like LiteFS and rqlite extend it to clusters, but they add operational complexity. The moment your app needs horizontal scaling for the database tier (not just the app tier), Postgres — especially managed offerings like Neon and Supabase — becomes the cleaner default. See our Supabase vs Neon comparison.
SQLite’s FTS5 module is excellent for basic full-text search. It hits a ceiling when you need semantic search, vector similarity, fuzzy matching with custom analyzers, or multilingual ranking. Postgres with the pg_trgm and pgvector extensions covers everything from typo-tolerant search to embedding lookups in a single database.
SQLite supports JSON via the JSON1 extension, but indexing on JSON fields is awkward. Postgres’s jsonb type with GIN indexes is the gold standard for storing and querying schemaless data alongside relational data. If half your tables are JSON columns you query by key, Postgres pays for itself.
Read scaling on SQLite is via the same disk as writes. If you need geographically distributed read replicas (analytics dashboards, customer-facing leaderboards, public read APIs), Postgres’s replication model is the standard tool. Managed Postgres providers offer this with one click.
If at least two thresholds apply, migrate. Don’t panic-migrate; the work is mechanical. Plan a half-day of focused engineering. Approximate steps:
sqlite3 app.db .dump > dump.sql. This produces a portable SQL file with schema and data.pgloader handle the conversion automatically; for small databases (<1GB) the SQLite dump can be converted by hand in 20 minutes.AUTOINCREMENT → SERIAL or BIGSERIAL; SQLite’s flexible typing → explicit Postgres types; strftime(…) → to_char(…). Your ORM (Prisma, Drizzle, etc.) handles most of these if it was Postgres-aware in code. Our Prisma vs Drizzle writeup covers ORM choices.DATABASE_URL from file:./app.db to your Postgres URL. Run schema migrations against the new database. Smoke-test in staging. Cut over.Cut over with a brief maintenance window: stop writes to SQLite, take a final dump, import to Postgres, switch the connection string, restart. For most solo SaaS apps under 100MB of data, this is a 15-minute downtime.
| Stack choice | Monthly cost | Setup effort | Operational tax |
|---|---|---|---|
| SQLite on a $6 VPS | $6 | Zero | File backup script |
| SQLite + Litestream replication | $6 + S3 | 30 min | Test restore quarterly |
| Postgres on Railway | $5–15 usage | 1 hour | Connection management |
| Postgres on Neon (free tier) | $0 | 30 min | Cold starts on free tier |
| Postgres on Supabase | $0–25 | 15 min | Auth+DB coupled |
| Postgres on AWS RDS | $50+ minimum | 3 hours | VPC, backups, IAM |
The right answer depends on your hosting decision. If you’re already on Vercel + Railway (see our Vercel vs Railway breakdown), Railway Postgres is the path of least resistance. If you want a generous free tier with cold starts, Neon. If you want auth bundled, Supabase — covered in our Supabase vs Firebase comparison.
Default to SQLite for any solo SaaS that runs on a single server, has a read-heavy load profile, and does fewer than ~100 writes per second. Migrate to Postgres the moment you hit two of the five thresholds. Don’t migrate before. The marginal complexity of running Postgres for an app that doesn’t need it is real engineering time you could spend on the product instead.
The stack, prompts, pricing, and mistakes to avoid — for solo founders building with AI.