SQLite in production when it makes sense

We earn commissions when you shop through the links below.

SQLite in production when it makes sense is one of those topics that still triggers strong opinions. Half the dev community treats SQLite as a toy for local development only. The other half — including some very serious engineers — are quietly running it at scale in production and loving it. The truth is nuanced: SQLite is the wrong choice for some architectures and exactly the right choice for others. This post breaks down when to embrace it, when to avoid it, and how to configure it properly if you go that route.

The case for SQLite in production

SQLite is the most widely deployed database engine in the world. It ships inside browsers, mobile apps, operating systems, and embedded devices. For years, the conventional wisdom was that it couldn’t scale for web apps — and for traditional multi-server, high-concurrency workloads, that’s still largely true. But modern deployment patterns have changed the calculus significantly.

Here’s what SQLite actually gets right:

  • Zero infrastructure overhead. No separate database server to provision, monitor, patch, or pay for. The database is a single file living on disk next to your application.
  • Incredible read performance. For read-heavy workloads with modest write rates, SQLite with WAL mode can outperform Postgres on the same hardware because there’s no network round-trip.
  • Atomic operations on a single node. SQLite’s ACID guarantees are rock-solid. You’re not giving up data integrity — you’re just trading distributed complexity for simplicity.
  • Dead-simple backups. Copy a file. Seriously. Or use the VACUUM INTO command for a hot backup.
  • Perfect fit for edge and single-tenant architectures. Each customer gets their own database file — isolation is trivial and noisy-neighbor problems disappear.

When SQLite in production actually makes sense

Let me be concrete. Here are the scenarios where SQLite in production when it makes sense isn’t just hype — it’s the pragmatic choice:

1. Single-server deployments

If your app runs on a single server and you don’t expect to need horizontal scaling, SQLite eliminates an entire layer of complexity. A Laravel or Rails app serving thousands of daily users on a single VPS is a completely valid architecture. You don’t need Postgres if you’re not using its multi-connection, networked nature.

2. Read-heavy internal tools and dashboards

Internal tools rarely need concurrent writes. If your team’s analytics dashboard runs a hundred reads per minute and a handful of writes per hour, SQLite is massively overspecified — but in the right direction. Simpler is better.

3. Per-tenant databases (multi-tenancy at file level)

This is where SQLite shines in a modern context. Instead of one big Postgres database with tenant isolation handled at the query level, you give each tenant their own SQLite file. The isolation is physical, migrations are per-tenant, and you can archive or delete a tenant’s data by deleting a file.

4. Edge deployments and Cloudflare Workers

Tools like Cloudflare D1 are built on SQLite precisely because the file-based, embedded nature fits the edge model. Each request can open a local SQLite database without network latency. This is a fundamentally different architecture than traditional web hosting, and SQLite is purpose-built for it.

5. CLI tools, background workers, and data pipelines

If you’re writing a data processing script, a CLI tool with persistent state, or a background job runner — SQLite is almost always the right call. Setting up Postgres for a cron job is engineering theater.

Essential configuration for production SQLite

Raw SQLite out of the box is not production-ready. You need to set pragmas correctly. Here’s the minimal configuration I use whenever I deploy SQLite to production:

-- Run these pragmas on every new connection
PRAGMA journal_mode = WAL;          -- Enable Write-Ahead Logging for concurrent reads
PRAGMA synchronous = NORMAL;        -- Fsync less aggressively (still safe with WAL)
PRAGMA busy_timeout = 5000;         -- Wait up to 5 seconds on lock instead of failing immediately
PRAGMA cache_size = -20000;         -- Use 20MB of memory for page cache
PRAGMA foreign_keys = ON;           -- Enforce FK constraints (off by default!)
PRAGMA temp_store = MEMORY;         -- Store temp tables in RAM
PRAGMA mmap_size = 134217728;       -- Enable memory-mapped I/O (128MB)

The most critical ones are journal_mode = WAL and busy_timeout. Without WAL, concurrent readers will block on writers. Without a busy timeout, you’ll get immediate lock errors under any write contention. Don’t ship without these.

If you’re using Laravel, you can set these in your database config:

'sqlite' => [
    'driver' => 'sqlite',
    'database' => env('DB_DATABASE', database_path('database.sqlite')),
    'prefix' => '',
    'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
    'options' => [
        PDO::ATTR_TIMEOUT => 5,
    ],
    // Run pragmas via a connection resolver or boot callback
],

For the pragmas themselves in Laravel, hook into the database connection event:

// In a ServiceProvider boot() method
DB::listen(function ($query) {});

Event::listen(\Illuminate\Database\Events\ConnectionEstablished::class, function ($event) {
    if ($event->connection->getDriverName() === 'sqlite') {
        $event->connection->statement('PRAGMA journal_mode=WAL');
        $event->connection->statement('PRAGMA synchronous=NORMAL');
        $event->connection->statement('PRAGMA busy_timeout=5000');
        $event->connection->statement('PRAGMA cache_size=-20000');
        $event->connection->statement('PRAGMA foreign_keys=ON');
        $event->connection->statement('PRAGMA mmap_size=134217728');
    }
});

Where SQLite falls short

I’m not going to pretend SQLite is always the answer. Here’s where you should reach for Postgres instead:

  • Multiple write-heavy application servers. SQLite only allows one writer at a time, and that writer must be on the same machine as the file. You cannot have two separate servers writing to the same SQLite file — that’s a recipe for corruption.
  • High concurrent write throughput. Even with WAL mode, if you’re doing thousands of writes per second across many connections, SQLite will struggle. Postgres is built for this.
  • Complex replication requirements. SQLite’s replication story (Litestream, LiteFS, Turso) is improving rapidly, but it’s still not as mature as Postgres streaming replication.
  • Full-text search at scale. SQLite FTS5 is decent, but Postgres with pg_trgm or an Elasticsearch sidecar is more capable for serious search workloads.

Backups: don’t sleep on this

The simplest SQLite backup strategy is Litestream — an open-source tool that continuously replicates your SQLite database to S3, Google Cloud Storage, or any S3-compatible storage. It runs as a sidecar process and streams WAL frames in real time. Recovery is a single command.

# litestream.yml
dbs:
  - path: /app/database/production.sqlite
    replicas:
      - type: s3
        bucket: my-app-backups
        path: sqlite/production
        region: us-east-1

For hosting, if you’re running a single-server SQLite app, platforms that give you persistent disk volumes are essential. Railway supports persistent volumes and makes it easy to deploy apps with attached SQLite storage — you get a modern deployment experience without giving up the simplicity of file-based databases. For more traditional VPS setups, Hostinger offers affordable single-server plans where SQLite shines since you control the whole machine.

The honest trade-off summary

SQLite in production when it makes sense comes down to one honest question: are you actually going to need horizontal scaling or high concurrent write throughput? For most SaaS products in their first few years, the answer is no. The operational simplicity of SQLite — no managed database costs, no connection pooling headaches, trivial backups, blazing read performance — is a genuine competitive advantage for small teams.

The engineers at Expensify, Notion (early days), and countless indie SaaS products have shipped SQLite in production successfully. The tooling around it — Litestream, LiteFS, Turso, and Cloudflare D1 — has matured enormously. SQLite is no longer a consolation prize for developers who can’t afford a proper database. In the right context, it’s the professional choice.

Use it where it fits. Don’t use it where it doesn’t. And configure those pragmas.