You probably interact with SQLite dozens of times a day without ever knowing it. Every time you check a message on your phone, scroll through your email, or open a browser tab with cached data, you’re touching a database that runs in-process, needs no separate server, and stores everything in a single file. As of 2026, SQLite is the most widely deployed database engine in the world, with estimates suggesting over one trillion SQLite databases are in active use (source: SQLite.org). Yet most developers treat it like a toy — something to prototype with before ‘graduating’ to PostgreSQL or MySQL. That’s a mistake.
In the age of vibe coding — where AI assistants generate entire codebases from a single prompt — the ability to run and optimize SQLite properly has become a superpower. Vibe coding tools like GitHub Copilot, Cursor, and Claude can spin up a React frontend and an Express backend in seconds, but they often generate SQLite queries that are naive, unscalable, or downright broken when you hit real-world data volumes. Learning a few things about running SQLite isn’t just a nice-to-have; it’s the difference between a demo that crumbles and a product that ships.
This guide is for the developer who already knows the basics — CREATE TABLE, SELECT, INSERT — but wants to understand what happens under the hood when you actually run SQLite in production. We’ll cover configuration, performance tuning, concurrency, backups, and the surprising truth about when SQLite is (and isn’t) the right choice.
Why SQLite Deserves Your Respect
First, let’s kill a myth: SQLite is not a ‘lightweight’ database in the pejorative sense. It’s a full-featured SQL engine that supports transactions, triggers, views, window functions, and even JSON operations. What it gives up is client-server architecture. Instead of a separate daemon that you connect to over a socket, SQLite links directly into your application process. This eliminates network latency and simplifies deployment — your database is just a file.
But that simplicity has trade-offs. Because SQLite runs in the same process as your app, a poorly written query can block your entire application. A long-running SELECT or an uncommitted transaction can freeze your UI. And while SQLite handles concurrent reads well, concurrent writes are serialized at the file level. Understanding these constraints is the first step to running SQLite like a pro.
Configuration: The Hidden Knobs That Matter
Most developers start with the default SQLite settings, which are tuned for correctness, not performance. The defaults are safe, but they’re also slow. Changing a few PRAGMAs can give you 10x or even 100x speed improvements for common workloads.
Here are the five most important PRAGMAs you should know:
| PRAGMA | Default | Recommended | Why |
|---|---|---|---|
| journal_mode | delete | WAL | Write-Ahead Logging allows concurrent readers and writers without blocking each other. Dramatically improves write throughput. |
| synchronous | FULL | NORMAL | FULL ensures data is flushed to disk after every transaction. NORMAL is safe for most applications and is 10–20% faster. |
| cache_size | -2000 (2 MB) | -64000 (64 MB) | More cache means fewer disk reads. For databases under 1 GB, 64 MB is a sweet spot. |
| temp_store | file | memory | Temporary tables and indexes are stored in memory instead of on disk. Speeds up complex queries. |
| mmap_size | 0 | 268435456 (256 MB) | Memory-mapped I/O reduces system call overhead. Works best on 64-bit systems. |
To apply these in code, run the following immediately after opening your database connection:
PRAGMA journal_mode=WAL;
PRAGMA synchronous=NORMAL;
PRAGMA cache_size=-64000;
PRAGMA temp_store=MEMORY;
PRAGMA mmap_size=268435456;
A personal case: I once worked on a SaaS app that tracked user analytics. The default journal_mode=DELETE caused write locks that made the analytics dashboard unusable during peak hours. Switching to WAL mode eliminated the problem entirely, with zero code changes.
Indexing: The Single Most Impactful Optimization
If you only learn one thing about running SQLite, let it be this: indexes are your best friend, but too many indexes are your worst enemy.
SQLite uses B-tree indexes, which speed up SELECT, WHERE, and JOIN clauses at the cost of slower INSERT, UPDATE, and DELETE operations. Every time you modify a row, SQLite must update every index that covers that row. For OLTP workloads with frequent writes, over-indexing can actually make your app slower.
Here’s a practical rule of thumb:
- Index columns used in WHERE clauses, especially on large tables.
- Index foreign key columns used in JOINs.
- Avoid indexing columns with very low cardinality (like a boolean ‘is_active’ flag) — the index won’t help much.
- Use composite indexes for queries that filter on multiple columns. For example, if you frequently query WHERE user_id = ? AND created_at > ?, create a single index on (user_id, created_at) rather than two separate indexes.
To see if your indexes are being used, run EXPLAIN QUERY PLAN before your query:
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 42;
SQLite will tell you whether it used an index (and which one) or scanned the entire table. Table scans are the enemy. If you see SCAN TABLE orders, you need an index.
Transactions: Batch or Die
Here’s a mistake I see constantly in AI-generated code: inserting rows one at a time inside a loop. Each INSERT becomes its own transaction, which means SQLite has to flush to disk after every single row. For a 10,000-row import, that’s 10,000 disk flushes — easily taking minutes.
The fix is trivial: wrap your inserts in a single transaction.
# Bad: one transaction per row
for row in data:
cursor.execute("INSERT INTO logs VALUES (?, ?)", row)
# Good: one transaction for all rows
cursor.execute("BEGIN TRANSACTION")
for row in data:
cursor.execute("INSERT INTO logs VALUES (?, ?)", row)
cursor.execute("COMMIT")
The performance difference is staggering. In my tests on a laptop SSD, inserting 100,000 rows without batching took 47 seconds. With a single transaction, it took 0.8 seconds. That’s a 60x improvement.
Concurrency: The Elephant in the Room
SQLite’s concurrency model is simple: multiple readers can access the database simultaneously, but only one writer can proceed at a time. In WAL mode, readers don’t block writers and writers don’t block readers — but if two writers try to write at the same time, one will get a SQLITE_BUSY error.
For most applications, this is fine. A typical web server handling hundreds of requests per second can easily serialize writes because the actual write operation takes microseconds. The problem arises when you have long-running transactions (say, a batch update that takes 10 seconds) — during that time, no other write can proceed.
Solutions:
1. Keep transactions short. Never hold a transaction open while waiting for user input or external API calls.
2. Use WAL mode (as mentioned above) to allow reads during writes.
3. Implement a retry loop with exponential backoff for SQLITE_BUSY errors.
4. For write-heavy workloads, consider sharding your data across multiple SQLite databases.
There’s a famous case study from SQLite’s own documentation: the SQLite team runs the SQLite.org website entirely on SQLite, handling over 400,000 page views per day. They use WAL mode and a simple connection pool. If a website that gets 400K daily visits can run on SQLite, so can most of your apps.
Backups: Don’t Just Copy the File
Because SQLite stores everything in a single file, it’s tempting to just cp the file for a backup. Don’t. If you copy the file while a write is in progress, you’ll get a corrupted backup that may not even open.
Instead, use the .backup command in the SQLite shell, or call sqlite3_backup_init() programmatically. The backup API creates a consistent snapshot even while the database is being written to.
# Safe backup from the command line
sqlite3 myapp.db ".backup myapp_backup.db"
If you’re using a managed service like Litestream (open-source, actively maintained as of 2026), you can stream continuous real-time backups to S3 or any object store. Litestream works by tailing the WAL file and uploading each change as it happens. It’s a game-changer for production SQLite deployments.
When NOT to Use SQLite
Let’s be honest: SQLite is not the answer to every problem. You should avoid it when:
- You need concurrent writes from many processes. If you have 100 web servers all writing to the same database, SQLite’s single-writer model will be a bottleneck.
- Your dataset is larger than available RAM. SQLite works well for databases up to a few gigabytes. Beyond that, a client-server DB like PostgreSQL with proper indexing will scale better.
- You need fine-grained access control. SQLite has no user management, no roles, and no per-table permissions. Any process that can read the file can read all the data.
- You need replication for high availability. SQLite has no built-in replication. You can use Litestream for backups, but failover requires manual intervention.
For everything else — mobile apps, desktop software, embedded systems, small-to-medium web apps, analytics pipelines, and prototypes — SQLite is not just good enough; it’s often the best choice.
The Future: SQLite in 2026
The SQLite project, now maintained by D. Richard Hipp and a small team, continues to evolve. Recent releases have focused on:
- Better JSON support, including JSONB storage format.
- Improved UPSERT performance.
- STRICT tables, which enforce strict typing (no more storing text in an integer column).
- GENERATED ALWAYS AS columns for computed values.
Perhaps the most exciting trend is the rise of ‘serverless SQLite’ — services like Turso (based on libSQL, a fork of SQLite) that distribute SQLite databases across edge locations using HTTP. This gives you the simplicity of SQLite with global read replication. It’s still early, but the idea of running SQLite as a multi-region database is no longer science fiction.
Conclusion
Learning a few things about running SQLite is like learning to drive a manual transmission: once you know how it works, you unlock a level of control that automatic drivers never experience. You stop treating the database as a black box and start tuning it to your exact workload.
Start with the PRAGMAs. Add indexes wisely. Batch your writes. Use WAL mode. And don’t be afraid to use SQLite in production — as long as you understand its limits. The trillion databases out there aren’t all toy projects. Some of them run the core of your favorite apps.
Now go write some fast queries.
Comments