SQLite Production API Concurrency: WAL Mode Limits [2026]

SQLite in production is real. WAL mode buys you read/write concurrency and great latency. The trap is write contention, checkpoints, and backups. Here’s the decision guide, tuning defaults, and a runnable load test + restore drill.

Part of theAI in Production series
a golden padlock sitting on top of a keyboard
Listen to this article
--:--

SQLite in a production API is a great idea. Right up until it isn’t.

My stance in 2026 is simple: SQLite in WAL mode is a legitimate default for single-node, read-heavy production APIs. You get absurd deployment simplicity, excellent read latency, and fewer moving parts than the typical “Postgres + migrations + connection pool + backups + replicas + …” stack.

But you do not get to ignore the physics: SQLite still has a single-writer ceiling. If your API is write-heavy, or you need multi-region writes, or you’re tempted by NFS “shared storage,” SQLite will punish you with tail latency spikes and SQLITE_BUSY right when your product starts to work.

Target keyword: SQLite production API concurrency WAL mode.

Key takeaways

  • SQLite WAL mode is safe for many production APIs, but it only scales cleanly when you design around a single-writer reality.
  • WAL improves concurrency because readers don’t block writers, yet write transactions are still serialized and can wreck p95/p99 latency under load.
  • Checkpointing is the production footgun. If you don’t tune and observe it, your WAL will grow and your latency will cliff.
  • Backups are not “copy the file.” In WAL mode, you need a real plan: .backup / Backup API, checkpoint discipline, and ideally WAL shipping.
  • If you need multi-writer or multi-region writes, stop bargaining. Use Postgres/MySQL, or add a coordination layer like LiteFS or a consensus system like rqlite.

What is SQLite WAL mode?

SQLite Write-Ahead Logging (WAL) mode is a journaling mode where SQLite appends changes to a separate -wal file and later checkpoints them back into the main database file. The win is that readers and writers can run at the same time on the same database.

graphs of performance analytics on a laptop screen

WAL is a big reason SQLite has turned into the “maybe we don’t need Postgres” database for the 2026 simplify-the-stack wave. Single-binary deploys. Edge workloads. Local-first apps.

Per D. Richard Hipp, WAL’s headline concurrency property is: readers do not block writers and writers do not block readers. True. Also easy to misunderstand.

Here’s the mental model I keep coming back to:

  • Reads come from a consistent snapshot of the database.
  • Writes append to the WAL.
  • Only one write transaction can commit at a time. SQLite serializes writers.

That last bullet is the whole story. Everything else is just what shape the pain takes.

Is SQLite WAL mode safe for production APIs?

Yes. It’s safe when you treat it like what it is: a single-node database with serious durability semantics and a strict write contention model.

A digital dashboard displaying marketing metrics including CTR and quality score on a screen

SQLite’s own docs have two extremely “production” truths that teams love to hand-wave:

  1. WAL is faster and more concurrent than rollback journal in most workloads. (D. Richard Hipp)
  2. WAL is not a network filesystem story. The docs are blunt: “WAL does not work over a network filesystem” because it requires shared memory coordination. (D. Richard Hipp)

So what’s actually safe?

  • A single VM running an API + SQLite on local SSD.
  • A container on one node with a real local disk.
  • A single-writer edge pattern (primary writes, replicas read) with explicit tooling.

What’s not safe?

  • Multiple API instances “sharing” the same SQLite file on NFS/EFS.
  • Any setup where you can’t guarantee local disk semantics and sane file locking.

SQLite isn’t the risky part. The weird deployment pattern is.

SQLite WAL mode concurrency: what you actually get

People ask “How many concurrent requests can SQLite handle?” as if there’s a magic number you can paste into a capacity plan.

monitor screengrab

That’s not how it fails in production.

The real split is:

  • Read-heavy: SQLite can take a lot of concurrent reads, and WAL keeps reads moving even while writes happen.
  • Write-heavy: you hit a ceiling quickly because write transactions are serialized.

Read-heavy APIs: where SQLite feels like cheating

For read-heavy endpoints, WAL is a gift.

If you have 200 concurrent requests hitting a GET /product/:id style endpoint, most of them can run concurrently as readers. WAL prevents a long-running read from blocking a writer, which is the classic rollback journal faceplant.

In practice, the limiter becomes boring stuff:

  • CPU time in your application
  • query plans and indexes
  • page cache behavior
  • disk latency if you’re thrashing

Not the lock.

Write-heavy APIs: the single-writer tax shows up as tail latency

In a write-heavy API, “concurrency” mostly means “queueing.”

Once enough writes pile up, you’ll see:

  • SQLITE_BUSY if you don’t wait
  • rising p95/p99 latency as writers contend
  • checkpoint work landing on the critical path

This is why “it worked in staging” is such a useless statement. Staging rarely has 50 clients all doing INSERT + UPDATE with sloppy transaction scope and real network jitter around the edges.

A practical rule I use: if your API regularly has double-digit concurrent write requests to the same hot tables, assume you’re going to have to earn SQLite. If you’re doing hundreds of writes/sec sustained, you’re probably shopping for Postgres unless your writes are tiny and your transactions are disciplined.

Why one writer matters for p95/p99

One writer means every write has to wait its turn. That wait time is what turns into user-visible tail latency.

If each transaction holds the write lock for 20ms and you have 30 writes queued, your worst-case request is already flirting with 600ms before you’ve even done any application work.

And if you do the classic mistake of wrapping extra logic in the transaction (validation queries, JSON parsing, network calls), you’ve turned a database lock into a distributed systems problem. For no reason.

WAL tuning for production: the PRAGMAs that actually matter

I’m going to keep this opinionated. Most “SQLite production” posts dump 20 PRAGMAs and call it guidance.

For a production API, the defaults I actually care about are:

  • journal_mode=WAL
  • synchronous=NORMAL
  • busy_timeout
  • wal_autocheckpoint

All of these are in SQLite’s PRAGMA reference. (D. Richard Hipp)

My baseline PRAGMA set

If you do nothing else, set these on connection init:

  • PRAGMA journal_mode = WAL;
  • PRAGMA synchronous = NORMAL;
  • PRAGMA busy_timeout = 5000;
  • PRAGMA wal_autocheckpoint = 1000;

What these do:

  • WAL: enables concurrent reads during writes.
  • synchronous=NORMAL: reduces fsync() pressure while keeping strong durability in WAL. This is one of those things where the boring answer is actually the right one.
  • busy_timeout=5000: makes SQLite wait up to 5,000ms for a lock instead of instantly failing with SQLITE_BUSY. That turns “random errors” into “bounded latency,” which is usually the correct trade.
  • wal_autocheckpoint=1000: checkpoints after 1,000 pages have accumulated in the WAL. It prevents unbounded WAL growth.

You’ll tune these based on your write rate and latency SLOs, but start with real numbers, not vibes.

Connection pooling and transaction scoping: the hidden `SQLITE_BUSY` multipliers

Two things amplify contention more than people expect:

  1. Too many connections. SQLite isn’t Postgres. You don’t want 100 pooled connections all “ready” to write.
  2. Long transactions. The fastest way to make SQLite look flaky is to hold a write transaction open while doing anything non-database.

Design rules that actually help:

  • Keep write transactions small and deterministic.
  • Don’t do “read, decide, write” loops inside a long transaction if you can express it as one statement or a carefully indexed upsert.
  • If you can, funnel writes through a single in-process queue. Yes, really. You’re acknowledging the single-writer model instead of pretending it isn’t there.

If you’re already deep in distributed complexity, you’re probably building microservices that each want their own DB anyway.

Checkpoint tuning: how you avoid WAL growth and latency spikes

Checkpointing is where WAL mode earns its “production sharp edges” reputation.

From the WAL docs: changes accumulate in the -wal file and periodically checkpoint back into the main database. (D. Richard Hipp)

Two failure modes show up in real APIs:

  1. Unbounded WAL growth
    • Your -wal file grows to hundreds of MB or multiple GB.
    • Restarts get slower.
    • Disk fills up.
  2. Checkpoint-induced latency spikes
    • A big checkpoint competes with foreground work.
    • p99 goes off a cliff.

What to watch in production

If you run SQLite in production, graph at least:

  • WAL file size (db.sqlite-wal bytes)
  • checkpoint frequency (events/min)
  • checkpoint duration (ms)
  • SQLITE_BUSY rate (count/min)
  • p95/p99 latency for write endpoints

If you don’t have these, you’re guessing.

Practical checkpoint strategies

You’ve basically got three knobs:

  • wal_autocheckpoint to keep WAL from growing forever
  • PRAGMA wal_checkpoint(TRUNCATE); during low-traffic windows to shrink WAL
  • scheduling an explicit checkpoint after batch jobs

One pragmatic pattern is: checkpoint after N writes or every T seconds, but only when you’re not already overloaded.

Do not checkpoint on every request. That’s how you reinvent rollback journal mode with extra steps.

Backups in WAL mode: safe, unsafe, and production-grade

“Just copy the SQLite file” is how you get corrupted backups and a bad weekend.

SQLite has an entire doc called “How To Corrupt An SQLite Database File,” and one of the canonical corruption patterns is backup or restore while a transaction is active. (D. Richard Hipp)

In WAL mode it’s even easier to screw up, because you’ve got multiple files involved (.db, -wal, -shm) and state transitions you don’t see until you’re trying to recover.

Tier 1 (simple): `sqlite3 .backup`

If you can tolerate a basic approach, use the SQLite shell .backup command. Under the hood, it uses safe mechanisms.

This is the minimum viable option for small apps.

Tier 2 (better): SQLite Online Backup API + scheduled checkpoint

If you own the application, the SQLite Online Backup API is the cleanest way to take consistent backups of a live DB.

As D. Richard Hipp explains, the API copies the source database into a destination file as a snapshot and can do it incrementally, reducing lock time.

Operationally:

  • run sqlite3_backup_* to a temp file
  • run PRAGMA wal_checkpoint(TRUNCATE); on a schedule (carefully)
  • upload the snapshot to object storage

Tier 3 (best for single-node): Litestream continuous WAL shipping

If you want something that behaves closer to “continuous replication,” Litestream is the pattern I see teams converge on.

Litestream’s whole value prop is streaming SQLite changes to object storage so you can recover to a recent transaction after a node loss. (Ben Johnson)

Treat it like this: SQLite on your node is fast. Object storage is your “oh no” button.

If you do this, you also need:

  • restore automation
  • periodic restore drills
  • integrity verification

Backups you’ve never restored are fiction.

Restore drills and verification (non-negotiable)

A restore drill is the only thing that matters.

At minimum, your runbook should include:

  • restore latest backup into a staging environment
  • run PRAGMA integrity_check; and fail if it returns anything other than ok
  • run an application-level sanity check (count key tables, check newest timestamp, resolve a known entity)

Do this at least monthly. If your business is real, do it weekly.

Also: keep at least two restore points. People love discovering their “latest backup” is broken.

Deployment patterns: where SQLite shines vs where it fails

This is the decision guide part. You’re here because you want to simplify your stack without getting paged.

Safe patterns

  • Single VM, local SSD, one API process. The boring winner.
  • Single container on one host with a persistent volume that is actually local to the node.
  • Primary-writer + read replicas using a coordination layer.

Unsafe patterns (don’t do this)

  • NFS/EFS/shared volumes. SQLite WAL explicitly does not work over network filesystems. (D. Richard Hipp)
  • Multiple writers across nodes without coordination.
  • Long-running transactions (background jobs, analytics queries) sharing the same DB as latency-sensitive endpoints.

What about edge SQLite (D1) and “distributed SQLite” tools?

2026 has made this more confusing because “SQLite” now includes managed and distributed layers.

  • Cloudflare D1 is SQLite-shaped, but it’s a managed service with edge constraints. It changes the operational story, not SQLite’s fundamental concurrency model. If you’re evaluating it, treat it like a platform product, not a file you control.
  • LiteFS is a distributed filesystem that replicates SQLite with a primary/replica lease model. Fly’s docs warn about unsafe combinations like autostop/autostart because lease ownership matters. (Fly.io)
  • rqlite uses Raft for HA. That buys you durability and failover, and it also tends to increase write latency because consensus is not free.

Useful tools. Same underlying constraint. One writer.

SQLite vs alternatives: the blunt comparison you actually need

Here’s the comparison table I wish more teams started with.

OptionWritesReadsHA / failoverMulti-regionOperational complexityWhen I’d pick it
SQLite (WAL) on one nodeSingle writer (serialized)ExcellentNo (by itself)NoVery lowRead-heavy APIs, local-first backends, cost-cut stacks
SQLite + LitestreamSingle writerExcellentRestore-basedNoLowSingle-node prod where fast recovery matters
SQLite + LiteFSSingle primary writerExcellent on replicasYes (lease failover)“Edge-ish” readsMediumEdge apps that can live with primary-writer semantics
rqliteConsensus writesGoodYesLimitedMedium-highWhen you need HA but can accept write latency
Postgres/MySQLMany writersExcellentYesYesMedium-highWrite-heavy APIs, multi-region writes, complex ops needs

A lot of teams should be in row 1 or 2.

A runnable load test harness (read-heavy vs write-heavy)

If you’re considering SQLite for a production API, measure your own workload. That’s the only honest answer.

I’m going to describe a harness you can drop into your repo. I’ll keep it language-agnostic, but I’ll assume you can stand up a tiny HTTP API.

What to build (minimum)

Build two endpoints:

  1. GET /items/:id (read-heavy)
  2. POST /items/:id/increment (write-heavy)

Schema:

  • items(id PRIMARY KEY, value INTEGER NOT NULL, updated_at INTEGER NOT NULL)

The write endpoint should do a single atomic statement like:

  • UPDATE items SET value = value + 1, updated_at = strftime('%s','now') WHERE id = ?;

Run two profiles

Use k6 (or wrk, vegeta) to run:

  • Read-heavy profile: 95% GET, 5% POST
  • Write-heavy profile: 50% GET, 50% POST

Track:

  • p50/p95/p99 latency
  • error rate
  • SQLITE_BUSY count

Concrete starting points:

  • 50 VUs for 60s
  • 200 VUs for 60s

That’s enough to surface contention behavior on a laptop and definitely on a small VM.

Compare rollback journal vs WAL

Run the same test twice:

  • PRAGMA journal_mode=DELETE; (rollback journal)
  • PRAGMA journal_mode=WAL; + the baseline PRAGMAs above

If WAL doesn’t materially improve your p95 on the read-heavy profile, your bottleneck isn’t SQLite journaling. It’s probably your query/indexes or app.

If WAL improves reads but your write-heavy profile gets p99 spikes or SQLITE_BUSY, congratulations. You just reproduced the real production failure mode before shipping it.

Red flags: when you should move off SQLite

This is the checklist I’d actually use in an architecture review.

Move to Postgres/MySQL (or add a coordination layer) if you hit any of these:

  1. You need multi-writer across instances.
  2. You need multi-region writes with low conflict.
  3. Your write workload includes long transactions (anything routinely over 50–100ms).
  4. You have a single hot table that gets hammered (counters, event ingestion) and you can’t batch or queue writes.
  5. You can’t guarantee local disk and correct file locking (Kubernetes + weird storage classes, NFS, “shared volume” hacks).
  6. Your p99 latency SLO is strict (say <200ms) and you’re already seeing lock waits.

Yes, you can engineer around some of these with queues, sharding by file, or moving hot writes into something like Redis. But at that point you’re rebuilding the database tier you were trying to avoid.

Simplicity is only a win if it stays simple while the business is growing.

Where I still like SQLite even at scale

SQLite is still a great choice when:

  • You can partition by tenant into multiple DB files.
  • Writes are infrequent or batchable.
  • You want local-first behavior and sync later.

It’s also fantastic as a per-service metadata store when your “real DB” is something else.

What I predict will happen next

In 2026, more teams will run SQLite in production because the economics are pushing everyone toward simpler stacks.

The next wave of incidents will look boring and painful: tail-latency outages caused by write contention and checkpoint storms in systems that were “fine” at 10% of their eventual traffic.

If you want the upside without the pager pain, do two things now. Run the load test before you commit. Practice the restore before you trust the backups.

That’s the difference between “SQLite is scary” and “SQLite is boring.”

Photo by Towfiqu barbhuiya on Unsplash.

Continue reading

Go code laptop screen developer — illustration for article on How to Upgrade to Go 1.27

How to Upgrade to Go 1.27 in Production [Week-1 Checklist]

A pragmatic Go 1.27 upgrade playbook: toolchain pinning, CI matrix changes, perf validation (p99/allocs/GC), migration traps, and what to do with json/v2.

Redis vs DragonflyDB 2026: Which Cache Actually Wins?

Redis vs DragonflyDB 2026: Which Cache Actually Wins?

I'd pick Redis for teams that need a battle-tested ecosystem and broad client support; I'd pick DragonflyDB if you're hitting Redis's single-threaded ceiling and want 25× throughput on the same hardware without a rewrite.

SQLite vs PostgreSQL 2026: Which DB Wins for App Backends?

SQLite vs PostgreSQL 2026: Which DB Wins for App Backends?

I'd pick SQLite for single-server apps under ~10k daily active users and PostgreSQL the moment you need concurrent writes, multi-node deployments, or a team larger than one. The fault line isn't size — it's concurrency and operational complexity.

a laptop computer sitting on top of a desk

How to Design Webhook Retries, Ordering, Idempotency [2026]

A practical 10M events/day webhook delivery architecture: partition keys, retry storm control, per-tenant isolation, idempotency storage, replay UX, and reliability SLOs.

Cite this article
Kunal Ganglani (2026, September 15). SQLite Production API Concurrency: WAL Mode Limits [2026]. Kunal Ganglani. Retrieved September 15, 2026, from https://www.kunalganglani.com/blog/sqlite-production-api-wal

Frequently Asked Questions

Is SQLite WAL mode safe for production APIs?

Yes, for many production APIs it’s safe, especially when you run on a single node with local disk and design around short write transactions. WAL improves read/write concurrency, but it doesn’t make SQLite a multi-writer database. The unsafe cases are usually deployment mistakes like shared network filesystems or multi-instance writes without coordination.

How many concurrent requests can SQLite handle in WAL mode?

For read-heavy traffic, SQLite can handle lots of concurrent requests because reads don’t block writes in WAL mode. For write-heavy traffic, throughput and tail latency are limited by the fact that write transactions are serialized. The real answer depends on transaction length, hot tables, and how many concurrent writers you allow.

Why does SQLite allow only one writer, and what does that mean for API tail latency?

SQLite uses file-level coordination and commits write transactions one at a time to preserve correctness and keep the engine small and reliable. In an API, that means concurrent writes queue behind each other. Under load, those waits show up as p95/p99 latency spikes and sometimes `SQLITE_BUSY` if you don’t configure lock waiting.

What is a safe backup process for SQLite in WAL mode?

Use SQLite’s built-in backup mechanisms such as the `.backup` command or the SQLite Online Backup API, which produces a consistent snapshot of a live database. Don’t rely on copying the database file while writes are happening, especially in WAL mode where `-wal` and `-shm` files may be involved. For stronger recovery, use continuous WAL shipping tools like Litestream and practice restores regularly.

How do Litestream and LiteFS differ, and when should you use them?

Litestream streams SQLite WAL changes to object storage so you can restore a single-node database to a recent point in time after a failure. LiteFS replicates SQLite to other nodes using a primary/replica lease model so reads can be local while writes go to the primary. Use Litestream for simple single-node durability; consider LiteFS when you need distributed reads and coordinated failover.

When should you move off SQLite to Postgres or MySQL?

Move when you need many concurrent writers, multi-region writes, or strict tail-latency guarantees under sustained write load. Also move if your deployment can’t guarantee local disk and correct file locking, such as shared network volumes. If you’re already adding queues, sharding, and coordination layers just to make writes behave, a full database server is often the cleaner choice.