DEV Community

Cover image for High-Performance SQLite Reads in a Go Server
Athreya aka Maneshwar
Athreya aka Maneshwar

Posted on

High-Performance SQLite Reads in a Go Server

Hello, I'm Maneshwar. I'm working on FreeDevTools online currently building **one place for all dev tools, cheat codes, and TLDRs* — a free, open-source hub where developers can quickly find and use tools without any hassle of searching all over the internet.*

SQLite is often underestimated in server environments.

Used correctly, it can deliver very high read throughput, even under concurrent access from many goroutines.

The key is understanding that SQLite is extremely fast at reads when you remove unnecessary safety overhead and align it with the OS page cache.

This article walks through practical SQLite tuning approaches that are commonly recommended and widely used for a read-heavy Go server that typically involves:

  • Many small point reads
  • Frequent full table scans
  • Concurrent access via goroutines
  • Single-process, server-side usage

These patterns and settings are based on SQLite documentation, community experience, and real-world reports, but you should validate them against your own workload before treating them as production-proven.

The goal: maximize read throughput and minimize latency.

Workload Assumptions

These recommendations assume:

  • Reads dominate (writes are rare or offline)
  • A single server process owns the database
  • Multiple goroutines issue concurrent SELECTs
  • Database size fits largely in RAM or OS cache
  • Durability across power loss is not critical

If these assumptions change, some tradeoffs below should be reconsidered.

1. Use WAL Mode (Non‑Negotiable)

The single biggest win for read concurrency is WAL (Write‑Ahead Logging).

PRAGMA journal_mode = WAL;
Enter fullscreen mode Exit fullscreen mode

Why this matters:

  • Readers never block writers
  • Readers never block other readers
  • Readers do not touch the main database file during reads
  • Pages are read sequentially from WAL + DB

For a read-heavy workload, WAL effectively turns SQLite into a lock-free reader engine.

If you are not using WAL, you are leaving a massive amount of read performance on the table.

2. Set synchronous = NORMAL

PRAGMA synchronous = NORMAL;
Enter fullscreen mode Exit fullscreen mode

In WAL mode, this is the sweet spot:

  • Transactions are still atomic and consistent
  • No extra fsync on every commit
  • Orders of magnitude fewer disk flushes

For read-heavy systems, durability across sudden power loss is rarely important. What matters is latency and throughput, and this setting removes a major I/O bottleneck.

3. Aggressively Increase Page Cache

SQLite’s default cache is tiny (~2MB). That is nowhere near enough for server workloads.

PRAGMA cache_size = -65536;  -- ~64MB per connection
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Negative values mean kilobytes, not pages
  • Cache is per connection
  • Larger cache reduces page faults and B-tree traversal cost

For mostly-read workloads, the cache directly translates into fewer disk reads and faster scans.

4. Enable Memory‑Mapped I/O (Huge Win)

PRAGMA mmap_size = 20000000000;  -- 20GB (or larger than DB)
Enter fullscreen mode Exit fullscreen mode

Memory-mapped I/O lets the OS page cache do the heavy lifting:

  • No read() syscalls per page
  • Kernel handles readahead automatically
  • Dramatically faster full table scans

If your database fits in RAM, this turns SQLite reads into near-memory-speed operations.

Rule of thumb: set mmap_size larger than your database file.

5. Keep Temporary Objects in Memory

PRAGMA temp_store = MEMORY;
Enter fullscreen mode Exit fullscreen mode

This avoids disk I/O for:

  • Sorts
  • GROUP BY
  • Temporary indices
  • Subquery materialization

For analytical or scan-heavy queries, this removes a silent but expensive bottleneck.

6. Use Exclusive Locking (Single‑Process Optimization)

If only one process accesses the database:

PRAGMA locking_mode = EXCLUSIVE;
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Fewer filesystem lock/unlock syscalls
  • Slightly lower latency per query
  • No shared-memory lock coordination

This is safe only when no other process needs database access.

7. Allow SQLite to Use Worker Threads

SQLite can parallelize some operations internally:

PRAGMA threads = 4;
Enter fullscreen mode Exit fullscreen mode

This allows:

  • Parallel scans
  • Faster large SELECTs
  • Better CPU utilization on multi-core machines

This pairs well with Go’s goroutine concurrency.

8. Index Like Your Throughput Depends on It (It Does)

No amount of PRAGMA tuning can save bad queries.

Guidelines:

  • Index every column used in WHERE, JOIN, ORDER BY
  • Avoid SELECT * on large tables
  • Verify plans using EXPLAIN QUERY PLAN

A single missing index can turn a 1ms read into a 200ms full scan.

9. Keep Query Planner Stats Fresh

PRAGMA optimize;
Enter fullscreen mode Exit fullscreen mode

When to run it:

  • After schema changes
  • After bulk data imports
  • Periodically on long-lived connections

This ensures SQLite chooses the fastest possible access path.

10. Read‑Only Mode for Extra Safety & Speed

If the database is truly immutable at runtime:

PRAGMA query_only = ON;
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Prevents accidental writes
  • Skips some write-related safety checks
  • Safer operationally

Recommended Baseline Configuration

PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -65536;
PRAGMA mmap_size = 20000000000;
PRAGMA temp_store = MEMORY;
PRAGMA locking_mode = EXCLUSIVE;
PRAGMA threads = 4;
PRAGMA query_only = ON;
Enter fullscreen mode Exit fullscreen mode

Apply these once per connection (except WAL which is persistent).

Go‑Specific Notes

  • Use a connection pool (database/sql)
  • Allow many read connections (they are cheap in WAL)
  • Reuse prepared statements for hot paths
  • Avoid serializing reads unnecessarily

SQLite scales extremely well with concurrent readers when configured correctly.

Final Takeaway

SQLite is not slow.

Misconfigured SQLite is slow.

With WAL, mmap, proper caching, and sane durability tradeoffs, SQLite can comfortably serve hundreds to thousands of reads per second from a single file, with minimal memory and operational complexity.

If your workload is read-heavy and your deployment is simple, SQLite remains one of the most efficient databases you can run.

FreeDevTools

👉 Check out: FreeDevTools

Any feedback or contributors are welcome!

It’s online, open-source, and ready for anyone to use.

⭐ Star it on GitHub: freedevtools

Top comments (0)