SQLite vs PostgreSQL (2026): An Honest Comparison With Our Own Benchmark

SQLite vs PostgreSQL (2026): An Honest Comparison With Our Own Benchmark

SQLite vs PostgreSQL is a different question from “MySQL or PostgreSQL”. There you compare two database servers. Here you compare a database server with a library that writes to a single file. The short answer up front: if your application runs on exactly one server and many processes rarely write at the same time, SQLite is almost always the simpler choice and often the faster one. If you need several application servers, many concurrent writers, heavy analytics or access over the network, pick PostgreSQL.

We run both in production. Our main server currently holds about 30 active SQLite files (games, small tools, stats services) and roughly a dozen PostgreSQL databases (shop software, a ticket system, a community platform). So this article isn’t built on gut feeling alone, on October 11, 2026 we ran both against exactly the same data: SQLite 3.51.3 (built into Node.js 22) and PostgreSQL 17.11 in the official Docker image, default settings, 2 million orders and 100,000 customers. Three results up front:

1. Fetching a single row by index: SQLite around 5 microseconds, PostgreSQL around 105 microseconds. Not a typo. SQLite skips the entire network round trip.

2. A big analytical query (join across 2 million rows, grouped by city): SQLite 2.9 seconds, PostgreSQL 0.3 seconds. On heavy queries PostgreSQL wins clearly, partly because it uses several CPU cores at once.

3. 16 processes writing at the same time: without the right setting, SQLite threw over 1.6 million “database is locked” errors in 5 seconds. With a single line of configuration it was 3. In the same test PostgreSQL managed 22,208 inserts per second, SQLite about 4,900.

A small glowing glass cube with a database inside stands next to a large server tower full of database cylinders, connected by a beam of light

SQLite vs PostgreSQL: The Fundamental Difference

The most important sentence of this article: SQLite is not a server. There’s no background service, no port, no user, no password. SQLite is a library compiled directly into your application. Your application opens a file, say data.db, and reads and writes it with SQL. The whole database, all tables, indexes and data, lives in that one file (plus two small helper files while it’s open).

PostgreSQL is a server. It runs as its own process, listens on a network port (5432 by default), manages users and permissions, and keeps its data in its own directory made of many files. Your application sends SQL over a connection to that server and gets results back. That works from the same machine, but just as well from ten other servers.

Almost everything that matters in this comparison follows from that:

  • Speed of small queries: SQLite reads straight from your own process’s memory. PostgreSQL has to package every request, send it, execute it in the server, and send it back. Even on the same machine that costs time per query.
  • Concurrent writes: A file that many processes want to write needs a lock. That’s why SQLite only ever allows one writer at a time. PostgreSQL coordinates many writers internally and lets them work in parallel.
  • Operations: You don’t install, start, secure, upgrade or monitor SQLite. You do all of that with PostgreSQL.
  • Multiple servers: Only programs on the same machine can use a SQLite file. Network filesystems like NFS are explicitly discouraged. Two application servers means you need a database server.

Minimalist illustration: one housing with a glowing database inside, next to several containers linked by cables, symbolising an embedded database versus a database server

Who is behind SQLite

SQLite was created in 2000 by D. Richard Hipp, originally for software aboard a US Navy ship that had to work without a database administrator. It is still maintained by a very small core team around Hipp. The code is in the public domain, not even under an open source license but completely free. The project accepts outside contributions only in a very limited way, and it is famous for extremely thorough testing: the test suite is many times larger than the code itself.

SQLite is probably the most widely deployed database in the world. It’s in every Android and iOS device, in Firefox, Chrome and Safari, in macOS, Windows 10 and 11, in aircraft, cars, TVs and countless desktop programs. When your browser remembers your history, it very likely does so with SQLite.

Who is behind PostgreSQL

PostgreSQL started in 1986 as a research project at the University of California, Berkeley and has been developed since the 1990s by the PostgreSQL Global Development Group, a community without a single owner. The license is the permissive PostgreSQL License. A major version ships every year and is supported for five years. We deliberately tested version 17 because that’s what most production setups run. If you want the history and the comparison with MySQL, we wrote it up in detail in PostgreSQL vs MySQL, also with our own benchmark.

Overview of the two opposite design philosophies by Source Compiler (September 2026).

Our Test Setup

So you can put the numbers in context, here’s exactly what we did. Everything ran on October 11, 2026 on our own server (12 virtual CPU cores, 23 GB RAM, NVMe SSD).

  • SQLite 3.51.3, directly inside the Node.js process via the built-in node:sqlite module. WAL journal mode (more on that shortly), otherwise defaults.
  • PostgreSQL 17.11 in the official postgres:17 Docker image, limited to 2 CPU cores and 2 GB RAM, default configuration. Accessed from Node.js via the pg library through the local Docker port.
  • Data: 100,000 customers (15 cities) and 2,000,000 orders with amount and date, generated from a fixed random seed, so bit-for-bit identical for both. Indexes on orders.customer_id and customers.city, followed by ANALYZE.

Two things about this setup aren’t fair, and we’d rather say so up front: SQLite had no CPU limit, but it only uses one core per query anyway. PostgreSQL sat behind Docker networking, which makes every individual request a bit slower than a direct Unix socket. Both, however, match pretty closely how most people actually run these two databases.

Benchmark Results: Reads, Analytics, Writes

TestSQLite 3.51PostgreSQL 17
Import 2.1M rows + indexes2.7 s10.9 s
20,000 primary-key lookups0.10 s (≈ 5 µs per query)2.15 s (≈ 108 µs)
20,000 “5 orders of one customer” queries0.10 s2.08 s
Group by month (2M rows)726–731 ms205–207 ms
Join + group by city (2M rows)2,879–2,938 ms265–299 ms
same, PostgreSQL without parallelism2,879–2,938 ms727–777 ms
5,000 updates, each committed individually0.89 s (FULL) / 0.14 s (NORMAL)1.36 s
Size on disk87 MB187 MB
Memory4–7 MB (entire CLI process)21 MB idle, 167 MB after the test

Single queries: SQLite is about twenty times faster

This is the part most people underestimate. A typical website mostly does one thing: fetch individual rows through an index. “Show me the user with this ID”, “show me this customer’s last five orders”. On exactly those queries, SQLite was about twenty times faster in our test: 20,000 queries in 0.1 seconds versus just over 2 seconds.

That’s not because PostgreSQL searches badly. Both find the row through the index in microseconds. The difference is the path: with PostgreSQL every query goes through the driver, over a network connection into the server, gets executed and travels back. In our setup that costs about 100 microseconds per round trip. SQLite just reads memory pages inside its own process.

What does that mean in practice? For a single page that runs ten queries, 1 millisecond versus 0.05 milliseconds doesn’t matter. It shows up in code that runs lots of small queries in a row, for example the infamous “N+1 problem” (loading the customer separately for each of 200 orders). With SQLite such a bug barely registers. With PostgreSQL it can easily make a page half a second slower.

Big analytical queries: PostgreSQL is three to ten times faster

As soon as a query has to touch millions of rows, the picture flips. For the join across all orders grouped by city, SQLite needed almost 3 seconds, PostgreSQL 0.3 seconds. Two reasons:

  1. Parallel queries. PostgreSQL spreads large queries across several CPU cores. With that turned off (max_parallel_workers_per_gather = 0), PostgreSQL needs about 0.75 seconds. Still four times faster than SQLite, but the gap halves.
  2. Better join strategies. SQLite solved this query with a nested-loop strategy: it walks all 100,000 customers and looks up each one’s orders through the index. PostgreSQL uses a hash join, reads both tables once and joins them in memory. SQLite has no hash join. For analytics over large datasets that’s a real disadvantage.

So if your project produces reports, statistics or dashboards over many millions of rows, PostgreSQL has a clear edge.

Writes: one setting decides everything

For 5,000 updates, each committed on its own, PostgreSQL needed 1.36 seconds. SQLite needed 0.89 seconds with the safe default synchronous=FULL, and just 0.14 seconds with synchronous=NORMAL.

What does that mean? With FULL, SQLite waits after every transaction until the data has actually hit the SSD. With NORMAL in WAL mode it only waits during the regular checkpoint back into the main file. The database always stays consistent. In the worst case, a power cut or operating system crash, the last committed transactions may be missing. Your application crashing on its own loses nothing. For most web applications NORMAL is a sensible compromise; for accounting or payments we’d stay on FULL.

SQLite was also ahead on import: 2.7 versus 10.9 seconds. To be fair: we loaded PostgreSQL with regular INSERT statements in batches of 2,000 rows. With the specialised COPY command PostgreSQL would have been much faster; in our MySQL comparison it loaded the same amount of data in 2.3 seconds that way.

SQLite’s Biggest Problem: Concurrent Writers

Now to the point where most SQLite projects first get into trouble in production. We let 1, 4 and 16 processes write into the same table as fast as possible for 5 seconds.

Concurrent writersSQLite without busy_timeoutSQLite with busy_timeout=5000PostgreSQL
15,237 / s, 0 errors5,192 / s, 0 errors3,855 / s
42,001 / s, 1,492,576 errors5,249 / s, 0 errors10,676 / s
16547 / s, 1,619,145 errors4,929 / s, 3 errors22,208 / s

Many small envelopes queue in front of a single narrow door, while next to it they pass through a wide gate with many lanes

Three things stand out.

First: the default is dangerous. Without busy_timeout, SQLite gives up immediately when another process is writing and reports database is locked (SQLITE_BUSY in some drivers). With 16 writers that was over 1.6 million failed attempts in 5 seconds, and only 547 inserts per second made it through. In a real application that error ends up in front of a user.

Second: one line almost completely fixes it. With PRAGMA busy_timeout = 5000; SQLite waits up to 5 seconds for the lock to free up. Suddenly even 16 writers push nearly 5,000 inserts per second through, practically error-free. That line belongs in every SQLite application with more than one process or thread. Many frameworks set it automatically now, but many don’t. Check yours.

Third: almost error-free isn’t error-free. With 16 writers we still saw three errors despite the timeout. That typically happens when a transaction first reads and then wants to write while another process is already writing. SQLite can’t wait there without both blocking each other, so it fails immediately. The practical fix: start write transactions with BEGIN IMMEDIATE instead of BEGIN. We didn’t dig into the exact trigger of those three errors; we mention it because it will show up in real-world use just the same.

And PostgreSQL? It gets faster with more writers, not slower. With one writer it was actually slower than SQLite (the network path again); with 16 writers it was four and a half times faster. If your application has lots of concurrent writes (chat, tracking, many users saving data at once), that’s the single most important reason for PostgreSQL.

For perspective: 5,000 writes per second is a lot. That’s 18 million per hour. An online shop with a thousand orders a day, a blog with comments or an internal tool won’t come anywhere near it. SQLite’s write limit is real, but it sits higher than most people think.

WAL Mode: The Most Important SQLite Setting

Almost everything that looks good for SQLite in this article assumes WAL mode (write-ahead log, introduced in 2010 with version 3.7.0). By default SQLite still uses the older “rollback journal”, where a writer also blocks all readers. In WAL mode, changes go into a side file (data.db-wal) first, and readers and the single writer no longer get in each other’s way.

PRAGMA journal_mode = WAL;      -- set once, persisted in the file
PRAGMA busy_timeout = 5000;     -- set on every connection
PRAGMA synchronous = NORMAL;    -- every connection, if you accept the trade-off
PRAGMA foreign_keys = ON;       -- every connection, otherwise foreign keys are ignored

We checked our own server: all our large production SQLite files run in WAL mode. That’s not luck, it’s the result of earlier mistakes.

Watch the last line: SQLite doesn’t enforce foreign keys by default. You can insert an order for a customer that doesn’t exist even though you defined a foreign key. Only PRAGMA foreign_keys = ON (per connection!) enforces it. That’s for backwards compatibility and surprises almost everyone coming from PostgreSQL.

Data Types: SQLite Is Lenient, PostgreSQL Is Strict

A test you can run yourself in ten seconds:

CREATE TABLE t (n INTEGER);
INSERT INTO t VALUES ('hello');
SELECT n, typeof(n) FROM t;

SQLite stores it without complaint and answers hello|text. A column you declared as an integer now holds a word. SQLite calls this “type affinity”: the column type is more of a suggestion. PostgreSQL rejects the same statement: invalid input syntax for type integer: "hello".

Since version 3.37 (2021) SQLite has STRICT tables. With CREATE TABLE t (n INTEGER) STRICT; SQLite behaves as you’d expect, and in our test it reported cannot store TEXT value in INTEGER column t.n. For new projects we recommend creating every table as STRICT. One caveat: STRICT tables only know INTEGER, REAL, TEXT, BLOB and ANY. A date, a time, a fixed-point decimal or a boolean has no dedicated type in SQLite. Store dates as text (2026-10-11) or numbers, and money preferably as integer cents.

PostgreSQL, on the other hand, has a huge type system: numeric for exact money amounts, timestamptz for timestamps with time zone, uuid, indexable jsonb, arrays, IP addresses, ranges, and through extensions geodata (PostGIS) or vectors for AI applications (pgvector).

Good news: schema changes are safe in both

One point where MySQL failed in our last comparison: if an ALTER TABLE inside a transaction is rolled back, MySQL keeps the change anyway. SQLite and PostgreSQL both roll back cleanly. We tested it: add a column, ROLLBACK, column gone again in both. That makes migrations pleasantly safe in either.

SQLite still has one limitation: ALTER TABLE can only add, rename and (since 3.35) drop columns. Changing a column’s type or adding a constraint afterwards isn’t directly possible. You create a new table, copy the data and swap the old one out. Most migration tools do this automatically, but on large tables it takes time.

Backups: The Trap Almost Everyone Falls Into Once

SQLite is a file, so you just copy it, right? No. We reproduced it: database in WAL mode, 1,000 rows written, file backed up with a plain copy command while the application was still running.

  • The live database contained 1,000 rows.
  • The copied file contained 0 rows.

The reason: the new data was still in the -wal side file and hadn’t been checkpointed into the main file yet. Copying only data.db gets you an old state, in the worst case a corrupt file. The nasty part: the copy opens fine, there’s no error, data is simply missing.

Two identical glowing glass cylinders on pedestals, connected by beams of light, symbolising a consistent database copy

How to back up SQLite properly:

# Consistent copy, even while the application is running
sqlite3 /path/data.db ".backup /backup/data-$(date +%F).db"

# Alternative: compact copy via SQL
sqlite3 /path/data.db "VACUUM INTO '/backup/data-$(date +%F).db'"

With .backup, our copy contained all 1,000 rows. That’s exactly how we back up our own most important SQLite file: via .backup, after which the script checks that the copy contains a minimum number of records. A copy nobody has ever opened isn’t a backup. How to turn that into a full strategy with copies in different places is covered in our article on the 3-2-1 backup strategy.

For SQLite there’s also Litestream, a small program that continuously replicates every change to S3-compatible storage. That gets you down to seconds of data loss in a disaster without running a second database server.

With PostgreSQL the same principle applies: copying the data directory while it’s running is a bad idea. The right tools are pg_dump for individual databases, pg_basebackup plus WAL archiving for point-in-time recovery, or tools like pgBackRest. More powerful, but also more work.

Benchmark by Anton Putra (January 2026). He tests with load over HTTP and reaches a similar result: SQLite strong on reads on a single server, PostgreSQL strong under concurrent write load.

Operations and Cost

SQLite: There’s nothing to operate. No installation (the library ships with Python, PHP, Node.js, Go drivers and almost everything else), no service, no port to secure, no passwords, no database server upgrades. Access control is the filesystem: whoever can read the file can read the database. Memory: in our test the entire SQLite command-line process used 4.5 MB idle and 6.5 MB while running a query across 2 million rows.

PostgreSQL: Its own service to install, configure, upgrade and monitor. With Docker that takes a few minutes (there’s a ready-made example in our Docker Compose article), but you have to handle major version upgrades, which in PostgreSQL require pg_upgrade or a dump and re-import. PostgreSQL is frugal with memory: 21 MB idle, 167 MB after our test because it caches data. How much RAM a server needs overall is something we measured in How much RAM does a server need?

Cost: Both are free. Managed offerings are a different story. A managed PostgreSQL database at a cloud provider easily adds 15 to 50 euros a month; a SQLite file costs nothing beyond disk space on the VPS you already have.

Disk space: The same data took 87 MB in SQLite and 187 MB in PostgreSQL. PostgreSQL stores more bookkeeping per row (among other things for versioning concurrent transactions), and our amount column was an exact numeric there instead of a floating-point number.

SQLite’s Limits, Honestly Listed

The SQLite developers themselves are very open about what their database isn’t meant for. In summary:

  • Multiple servers writing. A SQLite file belongs to one machine. For several application servers you need a database server or specialised tools like LiteFS or libSQL/Turso.
  • Many concurrent writers. See the test above: it works, but only one actually writes at a time. Long write transactions block every other writer.
  • Very large datasets with heavy analytics. The theoretical limit is around 281 terabytes per file; the practical limit is more the lack of parallel query execution.
  • Network filesystems. NFS, SMB and similar often have buggy file locking. The result can be a corrupted database.
  • Fine-grained permissions. There are no users and no table-level permissions.

What is not a limit, despite what you often hear: “SQLite is only for testing.” The official SQLite documentation gives a rough rule of thumb that websites with fewer than 100,000 hits a day run fine on SQLite, and stresses that this is a conservative estimate. The SQLite website itself runs on SQLite.

What We Use Ourselves (and Why)

We’ve ended up with a simple rule:

  • SQLite for everything that runs on one server with modest write traffic: our own analytics tool, status pages, forms, small game backends, price trackers, internal tools. Even our largest SQLite file (a game server with messaging, about 65 MB plus WAL) has run stably for months. All in WAL mode, all with busy_timeout.
  • PostgreSQL for shop software, our ticket system and the community platform. In other words wherever the software requires it, where several services share one database, or where we need strict types and extensions.

The most common mistake we used to make ourselves: reflexively spinning up a PostgreSQL container for every small project. That’s another service, another password, another upgrade, another backup, for projects that write a few hundred rows a day. The second most common mistake in the other direction: SQLite without WAL and without busy_timeout, and then wondering why “database is locked” shows up in the log under load.

SQLite vs PostgreSQL: Which Database Fits Whom

SQLite is the right choice if:

  • your application runs on a single server,
  • it’s mostly reads and writes are moderate (rule of thumb: well below a few thousand per second),
  • you want as little operational overhead as possible,
  • you’re building a desktop, mobile or embedded application (SQLite is practically the standard there),
  • you’re doing prototypes, tests or data analysis on your own machine,
  • you want to hand data over as one file.

PostgreSQL is the right choice if:

  • several application servers or services access the same data,
  • many users write at the same time,
  • you run heavy analytics over many millions of rows,
  • you need strict types, permission management or extensions like PostGIS and pgvector,
  • your software requires it,
  • you need high availability with replication and automatic failover.

A person stands at a fork in a path, one way leads to a small cosy house, the other to a city of connected server towers

Can I switch later?

Moving from SQLite to PostgreSQL is doable if you use a framework with a database abstraction (Django, Laravel, Rails, Prisma, Drizzle and similar). Tools like pgloader transfer a SQLite file into PostgreSQL with one command. The typical stumbling blocks: data that had the wrong type in SQLite (see above), dates stored as text, and queries using SQLite-specific functions. If you use STRICT tables and enable foreign keys from day one, a later move is much easier.

Checklist: SQLite or PostgreSQL in Five Questions

  1. Does the application run on more than one server? Yes → PostgreSQL.
  2. Do many users or processes regularly write at the same time? Yes, constantly hundreds per second or more → PostgreSQL.
  3. Do you need big analytics, geodata, vectors or strict types? Yes → PostgreSQL.
  4. Does your software require a specific database? Then use that one.
  5. None of the above? → SQLite, with WAL, busy_timeout, foreign_keys=ON, STRICT tables and backups via .backup.

Frequently Asked Questions About SQLite vs PostgreSQL

Is SQLite faster than PostgreSQL?

For individual small queries, yes, and by a lot: about twenty times in our test, because there’s no network round trip. On big analytical queries across millions of rows PostgreSQL is three to ten times faster, and also with many concurrent writers.

Can you use SQLite in production?

Yes, if the application runs on one server and write traffic is moderate. What matters is WAL mode, busy_timeout, foreign keys switched on and a consistent backup. We run about 30 production SQLite databases ourselves.

How many users can SQLite handle?

It’s not about the number of users but the number of writes. Reads scale very well. On writes we got about 4,900 inserts per second with 16 concurrent processes. The official rule of thumb calls websites with up to 100,000 hits per day unproblematic, and that’s on the cautious side.

What does “database is locked” mean?

Another process is writing and SQLite didn’t wait. Fix: set PRAGMA busy_timeout = 5000; on every connection, enable WAL mode, keep write transactions short and start them with BEGIN IMMEDIATE.

Do I need a server for SQLite?

No. SQLite is a library built into your application. The database is a file. All you need is the machine your application already runs on.

Can I just copy a SQLite file to back it up?

Only when the application is stopped. While it’s running you’ll miss data from the WAL file; in our test the copy contained 0 instead of 1,000 rows. Use sqlite3 data.db ".backup target.db" or VACUUM INTO.

Is SQLite secure?

SQLite itself is extremely well tested and very stable. But it has no users or passwords: whoever can read the file can read all the data. Security comes from file permissions. Encryption is only available through extensions like SQLCipher.

How big can a SQLite database get?

Theoretically around 281 terabytes. In practice databases of many gigabytes run without problems. The real limit is more that big analytical queries aren’t parallelised and schema changes on large tables take a long time.

What about Turso, libSQL, LiteFS or Cloudflare D1?

Those are projects that extend SQLite with replication across several servers or locations. Interesting for distributed applications, but each with its own operating model and trade-offs. For a single-server project you don’t need them.

How do I migrate from SQLite to PostgreSQL?

pgloader can transfer a SQLite file straight into PostgreSQL. Beforehand, check whether columns contain values of the wrong type and normalise your dates. With an ORM you often only need to change the connection URL in your code.

Which database is better for beginners?

For learning SQL, SQLite is unbeatable: nothing to install, open a file, go. Tools like DB Browser for SQLite show your data graphically. PostgreSQL is worth it as the next step once you build server applications.

Conclusion

SQLite vs PostgreSQL is less a question of “better” or “worse” than of architecture. SQLite is a file inside your application, unbeatably simple and about twenty times faster than PostgreSQL on small queries. PostgreSQL is a server that comfortably handles many concurrent writers, multiple application servers and heavy analytics.

Our recommendation: start small and medium projects on a single server with SQLite, but set it up properly (WAL, busy_timeout, foreign keys, STRICT, .backup). Switch to PostgreSQL as soon as multiple servers, many concurrent writers or heavy analytics come into play. And if you’re torn between PostgreSQL and MySQL, our PostgreSQL vs MySQL comparison will help.