SQLite vs PostgreSQL

Choose SQLite when the application can own a local database file and serialize its writes; choose PostgreSQL when independent clients need a shared database service and more concurrent write coordination. The difference is architectural. SQLite runs inside the application process, while PostgreSQL is a separate database server that clients connect to. Both can be appropriate production choices.

Avoid the rule that SQLite is only for demos or that PostgreSQL is always the necessary next step. SQLite can simplify a desktop tool, embedded device, local cache, or appropriately designed server application. PostgreSQL can simplify coordination when multiple application instances, reporting clients, and workers share authoritative data. Select from deployment and failure requirements rather than a vague application-size label.

TL;DR

Quick Example

This Python script demonstrates an isolated SQLite transaction and backup without leaving files behind. Save it as sqlite_example.py and run python sqlite_example.py. Python must include its standard sqlite3 module.

The example checks that the copied database can be read and contains the expected row. A production restore drill should also verify application queries, permissions, backup age, and recovery time. See Python's sqlite3 reference for connection and backup behavior.

Core Concepts

Embedded storage changes the deployment boundary

With SQLite, the application accesses its database through a library rather than a separate database server. That removes a service to provision and connect to, but ties storage ownership to the application's deployment. Decide which process or host owns the file, what survives a restart, and how another instance accesses the same logical data. SQLite's own appropriate-use guidance distinguishes local application storage from centralized client/server workloads.

With PostgreSQL, clients connect to a separate service. The service has credentials, networking, connection management, maintenance, and a recovery process. A managed offering can operate some of those layers, but the application still needs a plan for transactions, schema changes, query performance, and interrupted connections.

Concurrency is about write patterns, not just user counts

SQLite's WAL documentation explains how readers can continue while a writer appends to a log. WAL does not create multiple simultaneous writers to the same database. Long transactions and repeated write contention can cause busy errors and latency. A large read-heavy workload with short writes can behave differently from a small application with competing long write transactions.

PostgreSQL's MVCC model lets queries use consistent snapshots and reduces reader/writer interference. It does not mean writes to the same logical resource never contend. Design transactions to protect invariants and handle failures according to the chosen isolation level.

SQL support does not make engines interchangeable

SQL dialects, type behavior, generated identifiers, date handling, JSON functions, extensions, and schema-alteration workflows differ. ORM support can smooth basic CRUD while hiding differences until an important migration or concurrent operation fails. Keep integration tests on the actual production engine for the behavior the application depends on.

Head-to-Head

Do not turn this into a size threshold. The number of rows alone does not describe write contention, working-set memory, query shape, or recovery requirements. Test those factors directly.

When SQLite Fits Production

SQLite is worth evaluating for applications that should work without a network service: a desktop knowledge base, a device configuration store, a local analytical utility, or a field application with later synchronization. It is also a candidate for a server application with a clear single-host storage boundary and manageable writes. The operational simplicity is valuable when it matches the system's actual availability and scaling needs.

Before choosing it, trace every writer. An HTTP API may share the file with a scheduled importer, migration command, and background worker. A request rate that looks modest can still create contention if one importer holds a write transaction for a long time. Measure transaction duration and busy errors during realistic overlap, not only a benchmark with one client.

Keep transactions short and do network work outside the write transaction where correctness permits it. Configure bounded waiting and retry behavior intentionally; unlimited retries can hide overload and make requests accumulate. Decide whether a failed write can be retried safely without duplicating an operation.

Storage matters as much as query code. An ephemeral container filesystem is unsuitable for the only authoritative copy of a database. Mount durable storage deliberately and test replacement of the application process. Ordinary SQLite WAL requires its cooperating processes on the same host; do not assume placing the file on a network filesystem creates a shared database service. Consult the engine and filesystem constraints for the chosen deployment.

When PostgreSQL Is the Better Boundary

PostgreSQL is a strong candidate when several application replicas and independent tools need controlled access to one authoritative dataset. It separates application replacement from database ownership and provides server-side access controls and rich relational functionality. That boundary is often more natural than teaching each application instance how to coordinate access to a file.

It also introduces responsibilities. Set connection limits and pool sizes according to capacity rather than letting every worker open an arbitrary number of sessions. Run migrations with deliberate locking and rollback behavior. Observe query plans, lock waits, disk usage, and maintenance tasks. A managed service can help with infrastructure operations while leaving application-level correctness in your team's hands.

Choose availability features using the failure model. A read replica is not automatically a write failover system, and replication does not replace historical backups. Define who initiates failover, how clients reconnect, and what data loss is acceptable. Test these behaviors in a safe environment with realistic credentials.

Backups, Restore, and Migration

Back up the live database coherently

For SQLite, do not assume that copying only the main file while a live application writes produces a complete backup. WAL and transaction state matter. Use an engine-supported method such as the online backup API, then validate the result. Protect the backup and its encryption keys separately from the running application.

For PostgreSQL, select a recovery approach that matches the objective. Its backup documentation distinguishes SQL dumps, filesystem-level backup, and continuous archiving with point-in-time recovery. A successful backup command is only the first step; restoring the data and serving a representative request supplies stronger evidence.

Treat an engine migration as a behavior change

Inventory SQL and data assumptions before exporting. Verify uniqueness, foreign keys, nulls, identifiers, decimal precision, timestamps, collation, and transaction behavior. Test application reads and writes against the destination. Plan how to reconcile changes made during migration and how to reverse course without silently losing new data.

For SQLite specifically, enable and verify foreign key enforcement on the connections where it is required. Do not infer enforcement merely from declarations in a schema file. For either engine, compare business totals and constraint outcomes after import, not only raw row counts.

Best Practices

Decide who owns the data before scaling the application

Draw the application and storage topology. If every new application replica needs the same writable file, resolve that design before adding replicas. A database server may be the simpler coordination boundary; a deliberately partitioned local-storage design may be valid when its ownership rules are clear.

Exercise contention and restart behavior

Run the web workload alongside jobs and migrations. Record blocked operations and recovery after termination. Optimize the specific long transaction or query responsible for contention before assuming the only remedy is a new database engine.

Keep the restore procedure executable

Document commands, credentials, file locations, and validation queries. Periodically restore into an isolated environment and measure the result. An encrypted backup that nobody can decrypt during an incident does not satisfy a recovery objective.

Common Mistakes

Assuming WAL allows unlimited concurrent writers

Bad: enable WAL and remove all write-contention handling.

Correct: retain short transactions, bounded waits, safe retries, and monitoring. WAL improves reader/writer concurrency while preserving a single writer per database.

Confusing a file with a shared storage service

Bad: place the same SQLite database on an arbitrary shared drive for application replicas on several hosts.

Correct: choose a supported storage topology or a client/server database. Verify locking, durability, failure, and deployment requirements explicitly.

Assuming a connection-string change completes migration

Bad: point the ORM at PostgreSQL and publish without production-engine tests.

Correct: validate migrations, type behavior, constraints, concurrency, and rollback using realistic data before cutover.

FAQ

Can SQLite be used in production?

Yes, when deployment, durability, write concurrency, and recovery requirements fit its architecture. Production suitability is a property of the application design and operations, not a label reserved for database servers.

How many users can SQLite support?

There is no useful universal user count. Measure the rate and duration of writes, query costs, hardware, and contention. Read-heavy usage and competing long write transactions have very different requirements.

Does WAL remove database-locked errors?

No. WAL improves concurrent reading and writing, but there is still one writer at a time and busy conditions remain possible. Handle them with bounded policies and investigate long transactions.

Should I always develop on PostgreSQL if production uses it?

Use production-engine integration tests for engine-specific behavior. SQLite can remain useful for deliberately engine-independent local work, but a green SQLite test suite cannot prove PostgreSQL compatibility.

Related Topics

References