Skip to main content
Back to blog

Choosing a database for a small SaaS

·9 min readDatabasesWeb Dev

Most indie SaaS projects pick PostgreSQL on day one without thinking about it. It is the "safe default" and nobody ever got fired for choosing Postgres. But that does not mean it is the right choice for every project, especially small ones where you are the only developer and the only ops person.

Here is how I actually think through the database decision for small SaaS projects.

Start with the real question

The question is not "which database is best?" It is "how many servers will my application run on?" That single question eliminates most of the debate.

If your app runs on one server, SQLite is almost certainly the right starting point. If you need multiple servers writing to the same database, you need a client-server database. That is it. Everything else is details.

The thing most developers get wrong is overestimating how soon they will need multiple servers. A single VPS with 4 cores and 8 GB of RAM can handle a surprising amount of traffic. Thousands of concurrent users, easily. Most indie SaaS projects never outgrow a single server, and the ones that do usually take years to get there.

SQLite: the underrated option

I have written about SQLite before, so I will keep this brief. SQLite is an embedded database. No server process, no network overhead, no connection pooling to configure. Your database is a single file on disk.

For read-heavy workloads on a single server, SQLite is genuinely faster than PostgreSQL. There is no network round trip between your app and the database. Reads take microseconds instead of milliseconds. For a small SaaS doing mostly reads with occasional writes, that performance difference adds up.

Configuring SQLite for production

The default SQLite configuration is not optimized for web applications. These PRAGMAs make a significant difference:

PRAGMA journal_mode = WAL;          -- Concurrent reads + writes (non-negotiable)
PRAGMA synchronous = NORMAL;        -- Safe with WAL, avoids fsync on every commit
PRAGMA busy_timeout = 5000;         -- Wait 5s for locks instead of failing immediately
PRAGMA cache_size = -64000;         -- 64MB cache
PRAGMA foreign_keys = ON;           -- Enforce FK constraints
PRAGMA temp_store = MEMORY;         -- Keep temp tables in RAM

WAL mode (Write-Ahead Logging) is the most important one. Without it, readers and writers block each other. With it, you get concurrent reads alongside a single writer, which is how most web apps actually operate.

The busy_timeout is the second most critical setting. Without it, any concurrent write that hits contention throws an immediate error instead of waiting for the lock. Setting it to 5000ms dramatically reduces crash rates in production.

When SQLite's limits actually bite

SQLite allows many concurrent readers but only one writer at a time. The single-writer limit becomes a real problem when:

  • You have more than ~100 sustained write transactions per second
  • You have long write transactions that block other writers
  • You need multiple application servers writing to the same database

For a typical small SaaS (task manager, CRM, internal tool) doing a few writes per second with reads dominating, SQLite is more than sufficient. Companies like Tailscale, Expensify, and Signal run SQLite in production at significant scale. Tailscale migrated from etcd to SQLite because it simplified their architecture.

Backups: Litestream

The real concern with SQLite is durability. If your server dies, your database file goes with it. Litestream solves this. It continuously replicates your SQLite database to S3 (or any S3-compatible storage) by streaming WAL pages as they are written. You get point-in-time recovery without running a separate database server.

Litestream v0.5 (released October 2025) introduced a new file format with hierarchical compaction that dramatically improves restore times, and added point-in-time recovery to any moment in the past. It effectively solves the "but what about backups?" objection that used to push people toward Postgres prematurely.

PostgreSQL: when you actually need it

PostgreSQL is excellent software. I am not arguing against it. I am arguing against choosing it by default when you do not need it yet.

Running Postgres adds operational complexity. You need to manage a database server (or container), configure connection pooling, handle backups, monitor replication lag if you set up replicas, and deal with connection limits. For a solo developer shipping a SaaS, every piece of infrastructure you add is something you have to maintain at 2 AM when it breaks.

That said, there are legitimate reasons to start with Postgres:

  • You know from day one you will need multiple servers writing to the same database
  • You need advanced features like PostGIS for geospatial queries, full-text search with ranking, or JSONB indexing
  • Your write volume is genuinely high and you need fine-grained concurrency control
  • You need complex transactions spanning multiple tables with row-level locking

The key word is "know." Not "might need someday" but "know right now." Premature infrastructure decisions are one of the biggest time sinks for indie developers.

Managed databases: the landscape in 2026

The managed database market has shifted since PlanetScale removed their free tier in early 2024 (jumping from $0 to $39/month with no intermediate option). That triggered a mass migration of indie developers to other providers.

Neon (serverless Postgres)

Neon's free tier is the most generous for pure Postgres: 100 compute-unit-hours per month, 0.5 GB storage, and scale-to-zero so you pay nothing when idle. The killer feature is instant database branching, which gives you a full copy of your database for preview environments and CI/CD without the storage cost of a full clone.

Paid plans start at $19/month. After Databricks acquired Neon in May 2025, pricing dropped significantly.

Supabase (Postgres + backend)

Supabase is more than a database. It includes auth, realtime subscriptions, file storage, edge functions, and a REST API on top of Postgres. The free tier gives you 500 MB of database storage but pauses after 7 days of inactivity, which makes it unsuitable for always-on production.

The Pro plan at $25/month includes daily backups, 8 GB storage, and no pausing. It is a good option if you want a full backend platform rather than just a database.

Turso (managed SQLite)

Turso runs libSQL, a fork of SQLite that adds native replication. The standout feature is embedded replicas: your application syncs a local SQLite file that handles reads with zero network latency (microsecond reads), while writes go to the remote primary and propagate back automatically.

The free tier is generous: 5 GB storage, 100 databases, 500M row reads per month. Paid plans start at $5/month. If you like SQLite but want managed infrastructure and multi-region replication, Turso is the best option available.

Cloudflare D1 (SQLite at the edge)

If you are already on Cloudflare Workers, D1 is worth considering. It runs SQLite on Cloudflare's edge network with automatic global read replication. The free tier includes 5 million rows read per day and 5 GB storage.

The limitation is that D1 is designed for the Workers ecosystem. Each database maxes out at 10 GB, and it is best suited for a per-tenant or per-user database architecture rather than a single monolithic database.

Edge databases: the latency story

The trend in 2025-2026 is moving databases closer to users. The latency differences are significant:

SolutionRead latency
SQLite local file~0.01ms
Turso embedded replica~0.02ms
Cloudflare D1 from Workers~0.5ms
Managed Postgres (same region)5-15ms
Managed Postgres (cross-region)30-80ms

For read-heavy applications, the gap between local SQLite and a remote Postgres instance is two to three orders of magnitude. This does not matter for a dashboard that loads once, but it matters a lot for applications that make many database calls per page render.

ORMs make switching easier

One thing that reduces the stakes of this decision is using an ORM or query builder that supports multiple databases.

Drizzle is my current preference. It uses code-first schema definition in TypeScript (no separate schema language or code generation step), supports PostgreSQL, MySQL, and SQLite including serverless variants, and generates migrations from schema diffs. The query API maps directly to SQL, so you always know what the database is executing.

Prisma is the other major option. Prisma 7 (late 2025) was a significant improvement: 85-90% smaller engine, up to 3.4x faster queries, and 9x better cold starts after rewriting from Rust to TypeScript/WASM. It is more mature and has richer tooling, but Drizzle's SQL-like approach and tiny bundle size (7.4KB vs Prisma's larger footprint) make it a better fit for new projects.

If you start with SQLite and outgrow it, migrating to Postgres through either ORM is a weekend project, not a rewrite. Your queries stay the same, you update your connection config, run migrations, and move on.

The cost of managed vs self-hosted

Self-hosting looks cheaper on paper ($5-20/month for a VPS) but the hidden cost is operational time. For a small team, the realistic breakdown:

ApproachMonthly costOperational burden
SQLite + Litestream on VPS$5-20Medium: you manage backups, updates, monitoring
Turso Developer$5Low: managed, embedded replicas
Cloudflare D1$0-5Very low: managed, Workers ecosystem
Neon free tier$0Very low: managed, scale-to-zero
Supabase Pro$25Very low: full backend included
Self-hosted Postgres on VPS$5-20High: backups, upgrades, monitoring, HA

For most small teams, the $5-25/month premium for a managed service is worth it compared to the operational burden of self-hosting Postgres. The exception is SQLite + Litestream, where the operational burden is genuinely low because there is no database server to manage.

My recommendation

Start with SQLite unless you have a concrete reason not to. Configure it properly (WAL mode, busy_timeout, cache_size). Set up Litestream for backups on day one. Use Drizzle so migration to Postgres is painless later. Ship your product instead of configuring infrastructure.

If you want managed infrastructure without running a VPS, use Turso (if you like SQLite) or Neon's free tier (if you want Postgres). Both are genuinely free for small projects and scale gracefully as you grow.

When you hit the actual limits of SQLite (multiple servers, high write concurrency, advanced Postgres features), you will know. And at that point you will have real users and real revenue to justify the added complexity. That is a much better position to be in than running a Postgres cluster for an app with twelve users.

Sources

Enjoying the blog? Subscribe via RSS to get new posts in your reader.

Subscribe via RSS