Skip to main content
Home Creations Blog About Contact
Laravel & Architecture 6 min read

Why SQLite is Often the Best Database for Modern Creator Platforms

The case for SQLite is not that it scales further than you think. It is that the operational cost of the alternative is far higher than most small products can justify — and that cost is paid every month, forever.

R
R3X Software Engineer & Creator

Choosing Postgres for a new product is the default, and defaults are usually defensible. This one is worth examining, because the cost it carries is recurring and largely invisible at the moment you make the choice.

This article is about the decision. If you have already decided and want the operational specifics — WAL, busy timeouts, backups, the pragmas that matter — those are in running SQLite in production.

What a database server actually costs

The sticker price of a managed Postgres instance is the smallest part.

A network hop on every query. Even in the same availability zone, a round trip is 0.5–1.5ms. A page issuing 20 queries spends 10–30ms purely on network latency before the database does any work. SQLite runs in your process; the equivalent cost is a function call.

Connection management. A separate server means connection limits, pool sizing, too many clients errors, and eventually PgBouncer with its own configuration and failure modes. None of this exists when the database is a file.

A second thing to operate. Backups, restore testing, version upgrades, disk monitoring, failover behaviour, credential rotation. Each is manageable; collectively they are a standing tax on a small team's attention.

A second thing that can be down. Your application's availability is now the product of two systems' availability. For a solo project, the database being unreachable is one of the most likely causes of an outage — and it is a cause that simply does not exist when the database is on the same disk as the code.

For a large team these costs are absorbed by people whose job they are. For a creator platform, an indie SaaS, or an internal tool, they are paid by the one person who also has to build the product.

What creator platforms actually look like

The workload shape matters more than the scale, and this category has a consistent shape:

  • Reads dominate overwhelmingly. Public pages, feeds, profiles, documentation. Often 100:1 or higher.
  • Writes are bursty and small. A post published, a comment added, a setting changed. Not sustained concurrent write pressure.
  • Data volume is modest. Gigabytes, not terabytes. Millions of rows, not billions.
  • One application server is sufficient for a long time, and often forever.

That shape is close to ideal for SQLite. Reads never block in WAL mode and never leave the process. Writes are serialised, which is invisible when they are sporadic. The whole dataset fits in the page cache.

The performance argument, stated carefully

SQLite is not faster than Postgres at everything. It is faster at the specific thing most web requests do: a handful of indexed lookups returning few rows.

The reason is structural rather than clever. There is no network, no connection acquisition, no query serialisation across a socket, no result set marshalling between processes. A simple indexed read is a B-tree traversal in memory in your own address space.

Where Postgres is genuinely better: complex analytical queries with large joins, parallel query execution, sophisticated planning over many tables, and concurrent write throughput. If your product's core operation is a seven-way join with aggregation over ten million rows, that is a real reason to choose Postgres, and it is a different reason from "it scales better".

The honest limits

The case is only credible if the boundaries are stated plainly.

Multiple application servers. SQLite is a file. Two web servers cannot safely share one over NFS or EFS — the locking semantics network filesystems provide are not sufficient, and the failure mode is corruption rather than an error. If you need horizontal application scaling with shared state, you need a database server. This is the hard limit, and everything else is a matter of degree.

Sustained concurrent writes. One writer at a time, for the whole database. Sporadic writes are invisible; hundreds of independent writes per second from many connections will hit the ceiling.

Managed failover. There is no equivalent to Postgres streaming replication with automatic promotion. Litestream and LiteFS are good, and they are tools you assemble rather than a product feature you enable.

Very large datasets. SQLite handles hundreds of gigabytes technically. Whether you want your entire dataset on one machine's disk, with backup and restore times to match, is a separate question.

Deployment becomes trivial

This is the part that is hard to appreciate until you have lived on both sides.

# Backup, before a risky migration
cp database.sqlite database.sqlite.bak

# Copy production data to your laptop to debug
scp server:/var/www/app/database/database.sqlite ./local.sqlite

# Spin up a full staging environment
cp production.sqlite staging.sqlite

Debugging a production data issue means having the actual data open in your editor in thirty seconds. Testing a migration against real volume is a file copy. There is no dump, no restore, no credentials, no VPN, no waiting.

For a small team this changes how often you do these things, which changes how confident you are. That is a real engineering benefit even though it does not appear on any performance chart.

Litestream closes the durability gap

The strongest objection to SQLite in production is durability: one file, one disk.

dbs:
  - path: /var/www/app/database/database.sqlite
    replicas:
      - type: s3
        bucket: app-backups
        path: production
        retention: 720h
        sync-interval: 1s

Litestream streams WAL frames to object storage continuously, giving point-in-time recovery to any second within the retention window. It is one binary and one config file.

With it, the durability story is arguably better than an unreplicated managed instance: continuous shipping to durable storage, and restore is downloading a file rather than a database restore procedure.

It does not give you high availability — recovery requires restoring and restarting, which is minutes rather than seconds. Whether that matters depends on whether your product genuinely needs sub-minute recovery, which most creator platforms do not.

What the migration path looks like

The decision feels risky because it seems irreversible. It is not, and understanding the exit reduces the stakes considerably.

If you outgrow SQLite, the signals are unambiguous: SQLITE_BUSY errors that survive a generous busy_timeout, write latency climbing under normal load, or a product requirement for multiple application servers.

Migrating means moving data and changing a connection string. Using a query builder or ORM rather than raw SQL keeps the application layer almost entirely unchanged:

# Export
sqlite3 database.sqlite .dump > dump.sql

# Adjust the handful of SQLite-specific constructs, then load
psql app < dump.sql

The work is real — type differences, AUTOINCREMENT versus SERIAL, boolean representation — and it is a day or two, not a rewrite. Weigh that against paying for a database server from day one on a product that may never need it.

Some practices make it cheaper still: avoid SQLite-specific SQL, use the framework's schema builder, and keep business logic out of the database. Those are good practices regardless.

The decision, framed usefully

Ask three questions:

  1. Will this run on more than one application server? If yes, and soon, use a database server. This is the only hard blocker.
  2. Are sustained concurrent writes the core operation? A collaborative editor, a high-volume ingestion pipeline, a ticketing system with contention on the same rows — use a database server.
  3. Is anything else on the list a real requirement, or an anticipated one? Anticipated requirements are how projects acquire operational burden for scale they never reach.

If you answered no to all three, SQLite will very likely serve you well past the point where you would have found out, and the operational simplicity is worth real money and real attention every single month.

The failure mode worth naming: choosing SQLite for its simplicity and then building replication, connection brokering, and failover around it. At that point the simplicity argument has evaporated and you should have run Postgres from the start. If you find yourself assembling those pieces, that is the signal to migrate — not a reason to build a worse Postgres.

Related reading

Related articles

8 min read
Indie Engineering

The Solo Engineer's Playbook: Shipping Production Software

Working alone changes which engineering practices pay for themselves. The constraint is not skill or time — it is that you are the only person who will ever be paged, and everything follows from that.

#Architecture #DevOps #Indie Engineering
Read
10 min read
Systems Engineering

Zero-Downtime Laravel Deploys: The Parts That Actually Bite

Atomic symlink swaps are the easy half. The failures that take a site down mid-deploy come from opcache, queue workers running old code, and migrations that lock a table.

#Laravel #DevOps #Architecture
Read
8 min read
Laravel & Architecture

Killing N+1 Queries: Detection, Fixes, and Prevention

The N+1 query is the most common performance bug in ORM-backed applications, and it never shows up in development — because with twelve rows in your local database, nobody notices 13 queries.

#Laravel #Performance #Database
Read