Run Django on SQLite in Production

Run Django on SQLite in production safely: WAL mode, busy timeout and IMMEDIATE transactions, file placement and permissions, online backups, Litestream replication, and when to move on.

Problem statement

The standard advice is "never use SQLite in production". For many Django apps (internal tools, small SaaS products, content sites, single-server deployments with modest write volume), SQLite is fast, has zero operational overhead, and removes a network hop from every query. What fails is SQLite with default settings: concurrent writes produce database is locked, a copy of the file taken while the app runs can be corrupt, and the database is lost with the server if nothing replicates it.

This guide configures Django's SQLite backend for production, sets up safe backups and continuous replication, and lists the signals that mean it is time to move to PostgreSQL.

Production SQLite setupGunicorn workersone server onlySQLite WALreaders and one writerLitestreamstreams WAL changesObject storagepoint-in-time copiesSQLite suits one server; the WAL lets readers continue while a single writer commits.
  1. Use Django 5.1 or newer and set OPTIONS with transaction_mode: "IMMEDIATE", a timeout, and an init_command that enables WAL mode and sane pragmas.
  2. Store the database file outside the code directory, on a local disk (never a network filesystem), writable by the app user, with its directory writable too.
  3. Keep write transactions short; move slow work out of transaction.atomic blocks.
  4. Back up with the SQLite online backup API or VACUUM INTO, never by copying the file while the app runs.
  5. Stream changes to object storage with Litestream for near-real-time recovery.
  6. Move to PostgreSQL when you need multiple app servers, heavy concurrent writes, or database-level features SQLite lacks.

Step-by-step solution

1. Configure the database

# settings/production.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": "/srv/myapp/data/db.sqlite3",
        "OPTIONS": {
            "transaction_mode": "IMMEDIATE",
            "timeout": 5,
            "init_command": (
                "PRAGMA journal_mode=WAL;"
                "PRAGMA synchronous=NORMAL;"
                "PRAGMA foreign_keys=ON;"
                "PRAGMA mmap_size=134217728;"
                "PRAGMA cache_size=-20000;"
            ),
        },
    }
}
pragmas that make SQLite production-readyEffectjournal_mode=WAL✓readers never block the writersynchronous=NORMALsafe with WAL, much faster commitsbusy_timeout=5000wait for the lock instead of failingtransaction_mode IMMEDIATE✓write lock taken at BEGIN (Django 5.1+)Set them through OPTIONS so every connection gets themdjango-deployment.com
init_command in OPTIONS runs them on each new connection.

What each setting does:

  • journal_mode=WAL: writers append to a write-ahead log while readers keep reading the main file. Readers no longer block writers or the other way round. WAL mode is stored in the database file and persists.
  • transaction_mode: "IMMEDIATE": Django starts transactions with BEGIN IMMEDIATE, which takes the write lock at the start. With the default deferred mode, a transaction that reads and then writes can fail with database is locked immediately instead of waiting, because SQLite cannot upgrade the lock safely.
  • timeout: seconds to wait for the write lock before raising database is locked.
  • synchronous=NORMAL: safe with WAL (committed transactions survive application crashes; a power loss can lose the last transactions but not corrupt the database) and much faster than FULL.
  • mmap_size and cache_size trade memory for fewer reads; adjust for your server.

transaction_mode and init_command were added in Django 5.1. On older versions, set the pragmas in a connection_created signal handler.

2. Place the file correctly

sudo mkdir -p /srv/myapp/data
sudo chown myapp:myapp /srv/myapp/data
sudo chmod 750 /srv/myapp/data
where the database file livesLoses data✕inside the release directory✕in the container filesystem✕on NFS or other network storageSafe✓/srv/myapp/data/ on local disk✓a named volume on one host✓directory writable by the app userThe directory must be writable too: SQLite creates -wal and -shm files theredjango-deployment.com
One server only: SQLite cannot be shared across machines.

SQLite creates db.sqlite3-wal and db.sqlite3-shm next to the database, so the directory must be writable by the app user, not just the file. Keep the database:

  • outside the release directory, so deploys do not replace it;
  • on a local disk or block volume, never on NFS, SMB, or other network filesystems, where file locking is unreliable;
  • in a named volume or bind mount if the app runs in a container.

3. Keep write transactions short

Only one write transaction runs at a time. A view that holds a transaction open while calling an external API blocks every other writer for that duration:

# Bad: the write lock is held during a slow HTTP call
with transaction.atomic():
    order = Order.objects.create(...)
    payment_api.charge(order)
    order.mark_paid()

# Better: short transactions around the database work
with transaction.atomic():
    order = Order.objects.create(...)
result = payment_api.charge(order)
with transaction.atomic():
    order.mark_paid(result)

Avoid ATOMIC_REQUESTS = True with SQLite: it wraps each request, including slow parts, in one transaction.

Background workers (Celery, cron jobs) are additional writers. Batch their writes into small transactions.

4. Back up safely

Copying db.sqlite3 while the app is running can capture a half-written state, and ignores committed data still in the WAL file. Use one of the safe methods:

# Online backup API through the sqlite3 CLI
sqlite3 /srv/myapp/data/db.sqlite3 ".backup '/srv/myapp/backups/db-$(date +%F-%H%M).sqlite3'"

# Or a compacted copy
sqlite3 /srv/myapp/data/db.sqlite3 "VACUUM INTO '/srv/myapp/backups/db-$(date +%F-%H%M).sqlite3'"

Verify a backup by opening it and running PRAGMA integrity_check;. Ship backups off the server.

5. Replicate continuously with Litestream

Litestream runs next to the app, reads the WAL, and streams changes to S3-compatible storage, so you can restore to within seconds of a failure.

continuous backup with LitestreamDjango writes db.sqlite3WAL mode requiredLitestreamships WAL changes every secondS3-compatible bucketsnapshots plus WAL segmentslitestream restoreto any point it holdsA lost server costs seconds of writes, not a daydjango-deployment.com
Test the restore; replication you never restored is a guess.
# /etc/litestream.yml
dbs:
  - path: /srv/myapp/data/db.sqlite3
    replicas:
      - url: s3://myapp-backups/db
sudo systemctl enable --now litestream
sudo journalctl -u litestream -f

Credentials come from the environment (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or the config file. The configuration format has changed between Litestream releases, so check the documentation for the version you install.

Restore test (do this before you need it):

litestream restore -o /tmp/restore-test.sqlite3 s3://myapp-backups/db
sqlite3 /tmp/restore-test.sqlite3 "PRAGMA integrity_check;"

6. Deploy with migrations

Migrations take the write lock and may rebuild tables (SQLite implements many ALTER TABLE operations by copying the table). On large tables, that blocks writes for the duration. Take a backup before migrating, and run migrations at a quiet time for large changes.

a SQLite-safe deploy1Snapshot the databasesqlite3 .backup or Litestream2Switch codenew release directory3migrateone writer, quick4Restart Gunicornfew workers5Verify reads and writesa form, the adminNever copy the file with cp while the app is writingdjango-deployment.com
The .backup command produces a consistent copy while the app runs.

Verification check:

sqlite3 /srv/myapp/data/db.sqlite3 "PRAGMA journal_mode;"
ls -l /srv/myapp/data/
python manage.py dbshell <<< "PRAGMA foreign_keys;"

Expect wal, the -wal and -shm files owned by the app user, and 1 for foreign keys.

Explanation

SQLite is an in-process library: queries run inside your Gunicorn workers with no network round-trip, which makes reads extremely fast. Its concurrency model is many readers and one writer at a time. WAL mode makes readers and the writer independent, BEGIN IMMEDIATE makes writers queue predictably instead of failing, and the timeout sets how long they queue. Because the database is a file on one machine, durability depends entirely on your backup and replication setup, which is what Litestream provides.

Edge cases / notes

When to move to PostgreSQL

Switch when you need more than one application server, when write contention shows up as lock timeouts under normal load, when migrations on large tables block for too long, or when you need features such as row-level locking with select_for_update(skip_locked=True), full-text search at scale, or managed high availability. Moving is easier early: use dumpdata/loaddata for small databases or a dedicated migration tool for larger ones.

Containers and platforms

Platforms with ephemeral filesystems or multiple instances (most PaaS offerings, autoscaled containers) are a poor fit unless the platform provides a persistent volume attached to exactly one instance.

When to automate this

Automate the backup schedule and, more importantly, a regular restore test that downloads the latest replica, runs integrity_check, and reports the result. A backup that has never been restored is an assumption.

Next steps

FAQ

Is SQLite good enough for a production Django site?

For single-server apps with moderate write volume, yes, when configured with WAL mode, immediate transactions, a busy timeout, and real backups. It is not suitable for multi-server deployments.

Why does Django raise "database is locked" with SQLite?

Another connection holds the write lock longer than the timeout, or a deferred transaction tried to upgrade to a write lock. Use transaction_mode: "IMMEDIATE", a timeout, and short write transactions.

Can I back up SQLite by copying the file?

Not while the application is running. Use the .backup command, VACUUM INTO, or Litestream.

Can several Gunicorn workers share one SQLite database?

Yes. Multiple processes on the same machine can use one database file safely; writes are serialised. Multiple machines cannot.

Keep going