When to Move From SQLite to Postgres for a Growing App

A growing SMB app hitting its first real concurrency problem faces a decision that's easy to get wrong in either direction: migrate to a client/server database too early and take on operational overhead the app doesn't need yet, or stay on SQLite too long and let write contention turn into user-facing timeouts. The right call isn't about which database is "better" in the abstract — SQLite and PostgreSQL are built to solve different problems, and picking between them comes down to reading a small number of concrete signals correctly rather than guessing at future scale.

This guide walks through SQLite's own published guidance on where it's the right engine, where a client/server RDBMS like PostgreSQL takes over, and a concrete migration-trigger checklist so a growing SMB app can make the call deliberately instead of after write contention has already forced the question.


What SQLite Itself Says It's Built For

SQLite's own documentation draws the line plainly: it "is not directly comparable to client/server SQL database engines such as MySQL, Oracle, PostgreSQL, or SQL Server since SQLite is trying to solve a different problem." Client/server engines are built to implement a shared repository of enterprise data and emphasize scalability, concurrency, centralization, and control. SQLite is built to provide local data storage for individual applications and devices, and emphasizes economy, efficiency, reliability, independence, and simplicity instead. Its own documentation frames the comparison memorably: SQLite doesn't really compete with client/server databases at all — it competes with a plain file on disk, opened with fopen().

That framing explains the list of situations SQLite's documentation says it handles well: embedded devices and IoT hardware that need to operate without expert human support, an application's own on-disk file format, a local cache in front of a slower enterprise database, and — notably — most websites. SQLite's documentation states that it works great as the database engine for most low-to-medium traffic websites, and offers a specific, conservative benchmark: any site getting fewer than 100,000 hits a day should work fine, a figure the documentation calls conservative rather than a hard ceiling, noting SQLite has been demonstrated to handle ten times that volume. The concurrency mechanics behind that claim are specific too: SQLite supports an unlimited number of simultaneous readers, but allows only one writer at any instant. For most applications this isn't a bottleneck — writers simply queue, each write transaction typically finishes in a few dozen milliseconds, and the queue clears fast enough that nobody notices. That single-writer, multi-reader model is the mechanical reason SQLite is a genuinely good fit for a large share of small and mid-sized applications, not just a "good enough for now" placeholder.

Where a Client/Server RDBMS Like PostgreSQL Wins

SQLite's documentation is equally direct about where that single-writer model stops being the right fit, and lists the situations by name. If many client programs need to send SQL to the same database over a network at once, SQLite's documentation recommends a client/server engine instead — SQLite can technically work over a network filesystem, but the documentation warns that file-locking behavior in many network filesystem implementations is unreliable enough to risk database corruption when multiple clients write at once. High-volume, write-intensive websites that need to scale across multiple application servers get the same recommendation. And while SQLite technically supports database files up to 281 terabytes, its own guidance suggests moving to a centralized client/server database once content "looks like it might creep into the terabyte range" — well before hitting that technical ceiling, since most filesystems and single-file designs weren't built to comfortably manage files that large in production.

PostgreSQL is the client/server RDBMS most often named as SQLite's counterpart, and its own documentation reflects exactly the concurrency and scale capabilities SQLite's guidance points toward. PostgreSQL's manual dedicates a full chapter to Concurrency Control, documenting a multiversion concurrency control (MVCC) model that lets multiple transactions read and write against a database at the same time without the single-writer queueing SQLite relies on. A separate chapter on High Availability, Load Balancing, and Replication documents built-in mechanisms for spreading a workload and its data across multiple physical servers — a structurally different approach from SQLite's single-file, single-process design, and the direct answer to the "many concurrent writers" and "very large datasets" cases SQLite's own guidance flags as its limits. As of this writing, PostgreSQL's maintainers — the PostgreSQL Global Development Group — document version 18 (specifically 18.6) as the current release, with major versions 17 through 14 still actively supported in parallel, reflecting an ongoing multi-version maintenance model built around a long-lived server process rather than a bundled library.

A Concrete Migration Trigger Checklist for SMB Apps

SQLite's documentation includes its own short checklist for choosing a database engine, and it translates directly into signals a growing SMB team can check against real production behavior rather than a hypothetical growth curve.

Is the database now on a different machine than the application code? SQLite's own checklist calls this out specifically: SQLite is built into the application process, so if the data lives on a separate device, the high-bandwidth link between the database engine and the disk has to cross the network — a link the documentation says works but performs suboptimally. Once an app has scaled to multiple application servers that all need to reach one shared database over the network, that's a structural reason to move to a client/server engine, independent of how busy the database actually is.

Are writes genuinely blocking each other, not just queuing? SQLite handles queued writers well — most transactions clear in milliseconds, and a lot of production traffic never notices the single-writer model. The real signal is measurable write contention: locking errors or write timeouts that show up in logs during normal traffic, not a hypothetical worst case. That's the point where SQLite's queue is no longer clearing fast enough for the app's actual write volume.

Is the dataset creeping toward the terabyte range? SQLite's technical ceiling is far above what most SMB apps will ever reach, but its own documentation recommends considering a client/server engine well before that ceiling, once content size starts approaching a terabyte — the point where a single-file design and typical filesystem limits start working against the app rather than for it.

Does the app need built-in replication or read replicas across multiple servers? This is a capability gap, not a performance one. SQLite has no built-in equivalent to PostgreSQL's High Availability, Load Balancing, and Replication chapter; an app that needs geographically distributed read replicas or automatic failover across servers needs a database designed for that from the ground up.

None of the above yet? SQLite's own checklist ends with "Otherwise, choose SQLite" — for device-local storage, low writer concurrency, and sub-terabyte content, its documentation states plainly that SQLite is "almost always a better solution," precisely because it requires no configuration or maintenance and just works.

How to Decide Without Guessing

The practical mistake in either direction is deciding on a hypothetical growth curve instead of measured behavior. Migrating to Postgres before an app has hit any of the signals above adds real operational cost that SQLite doesn't carry — a long-running server process, connection pooling, and backup and replication operations to manage — for a concurrency ceiling the app was never going to hit. Staying on SQLite past the point where writes are visibly queuing or blocking, on the other hand, turns a database decision into a production incident.

The reliable approach is to check actual production signals against SQLite's own checklist rather than reason from an abstract worry about scale: pull a week of real write-error logs, check whether the app's data footprint is genuinely approaching a terabyte, and confirm whether the architecture has actually moved to multiple servers that need to share one database over a network. An app that isn't hitting any of those signals yet is very likely still in the range SQLite's own documentation says it's built for — and a lot of production software, including SQLite's own project website, runs on exactly that model for years before, if ever, outgrowing it.

Key Takeaways

  • SQLite is built for local, single-writer application storage; a client/server RDBMS like PostgreSQL is built for shared, concurrent, network-accessed data — they solve different problems rather than compete on the same axis.
  • SQLite allows unlimited simultaneous readers but only one writer at a time; most apps never notice because writes typically clear in a few dozen milliseconds, but measurable write contention is the real signal to watch, not a guess about future traffic.
  • SQLite's own guidance recommends a client/server engine once a dataset creeps toward the terabyte range, well before its technical 281-terabyte file limit.
  • PostgreSQL's documented MVCC concurrency model and built-in high-availability/replication chapter are the direct answers to SQLite's stated limits on concurrent writers and multi-server scale.
  • Check real production signals — write-error logs, actual data size, and whether the app now spans multiple servers sharing one database — before migrating in either direction; SQLite's own checklist ends with "otherwise, choose SQLite" for a reason.

References

  • Appropriate Uses For SQLite — SQLite's own documentation on where it's the right fit, where a client/server engine works better, and its checklist for choosing between them.
  • PostgreSQL 18 Documentation — the official manual for the current PostgreSQL release, including the chapters on concurrency control and high availability referenced in this guide.

Posts in this series