Memory-Mapped I/O Configuration

When a SQLite deployment leaves mmap_size at its factory default of 0, every page the engine reads travels through the traditional read() syscall path: a kernel-to-user copy for each page, a context switch on every cache miss, and redundant buffering between the OS page cache and SQLite’s own page cache. On constrained ARM SoCs, sub-millisecond desktop UI threads, and high-throughput Python batch pipelines, that overhead surfaces as unpredictable read latency, inflated CPU-in-iowait, and accelerated flash wear from duplicated cache traffic. Memory-mapped I/O eliminates the copy by mapping database pages directly into the process virtual address space — but only for the main database file, and only when the value is calibrated against physical RAM and address-space limits. This configuration is part of the broader WAL Optimization & Concurrency Tuning discipline, and it must be coordinated with checkpoint cadence and page-cache sizing rather than tuned in isolation. Set it too high on a 32-bit gateway and you invite SIGBUS, OOM kills, and swap thrashing; leave it disabled on server-class hardware and you throttle analytical scans that would otherwise run at memory bandwidth.

Core Mechanism & Crash-Safety Defaults

PRAGMA mmap_size=N instructs SQLite to mmap() up to N bytes of the main database file into the process address space. Once mapped, page reads become ordinary memory dereferences serviced by the kernel’s demand-paging machinery: the first touch of an unmapped page triggers a minor fault that maps the underlying file block, and subsequent accesses hit RAM directly with zero syscall overhead. Writes to mapped pages still go through the normal write path — SQLite treats the mapping as read-mostly and does not rely on dirty-page writeback for durability.

Critically, memory mapping never covers the write-ahead log. SQLite’s crash-recovery model depends on deterministic, ordered writes to the WAL followed by an fsync() before frames are considered durable. Mapping the log would introduce non-deterministic page-fault write ordering and break the atomicity guarantee that lets the engine recover a consistent database after abrupt power loss. That is why mmap_size accelerates read-heavy and analytical workloads but does nothing for commit latency. The crash-safety defaults you set elsewhere — PRAGMA journal_mode=WAL and PRAGMA synchronous=NORMAL, covered in the PRAGMA Optimization Guide and the synchronous crash-safety page — remain fully in force and are unaffected by the mapping.

Two subtleties govern safe operation. First, the OS may silently cap the requested size: a 32-bit process cannot map more than its fragmented address space allows, and the kernel enforces vm.max_map_count. SQLite honours whatever the kernel grants and reports the effective value back through PRAGMA mmap_size, so the requested and applied numbers can differ. Second, the map is a live window onto the file: if a concurrent checkpoint or a VACUUM shrinks the database while pages are mapped, dereferencing a now-truncated region raises SIGBUS. Both behaviours make read-back verification mandatory rather than optional.

Read data path: mmap_size=0 versus mmap_size=N Two stacked panels compare how database pages reach SQLite. With mmap_size=0, each page read crosses the user/kernel boundary through a read() syscall, copying the block from the OS page cache into SQLite's own page cache — the same page is buffered twice. With mmap_size=N, the main database file is memory-mapped directly into the process virtual address space, so a page read is a zero-copy memory dereference serviced by demand paging. The write-ahead log is never mapped and continues to use fsync-ordered writes for durability. mmap_size = 0 · default read() syscall path SQLite process · user space kernel · OS page cache + disk SQLite page cache copy #1 · user RAM OS page cache copy #2 · kernel RAM DB file read(): copy + context switch each cache miss → syscall + kernel→user copy · same page buffered twice mmap_size = N · memory-mapped zero-copy path SQLite process · user space kernel · storage mapped window in virtual address space page read = direct deref main DB file -wal log mmap(): zero-copy demand paging writes bypass the map → -wal stays fsync-ordered, never mmap'd

Step-by-Step Implementation

1. Verify Prerequisites and PRAGMA Baselines

Memory mapping is a layer on top of an already-hardened connection. Confirm WAL mode is active, the page cache is sized deliberately, and inspect the mapping the engine is currently willing to grant before changing anything:

PRAGMA journal_mode;      -- expect: wal (mmap accelerates reads regardless, but WAL is the baseline)
PRAGMA page_size;         -- typically 4096; needed to reason about page counts vs bytes
PRAGMA cache_size;        -- negative = KiB of page cache; overlaps with mmap, so size them together
PRAGMA mmap_size;         -- current effective mmap window in bytes; default 0 = disabled

If page_size reports a non-default value (e.g. 8192 on some embedded images), factor it into the threshold math below. Because both cache_size and mmap_size hold copies of pages in RAM, calibrating them jointly avoids double-buffering — the interaction is detailed in Tuning cache_size for Embedded Linux.

2. Select the Target Value

Pick the map size from the host’s physical RAM and address width, not from the database size. A useful starting formula is:

target_mmap_bytes = min(database_file_size, 0.25 × physical_RAM_bytes)

Cap at 25% of RAM on memory-constrained or swap-disabled devices; there is no benefit to mapping more of the file than fits alongside the working set. Use the decision table to pick a concrete value:

Host profile Physical RAM mmap_size (bytes) Rationale
32-bit ARM SoC ≤ 512 MB 67108864 (64 MB) Stays well under fragmented 32-bit address space; avoids vm.max_map_count pressure
64-bit edge gateway 1–2 GB 134217728268435456 (128–256 MB) Covers hot working set without starving app heap
Desktop / NVMe ≥ 4 GB 2684354561073741824 (256 MB–1 GB) Accelerates analytical scans at memory bandwidth
High-memory server ≥ 16 GB 0 or matched to cache_size Large page cache already resident; avoid duplicate caching layers

3. Apply Configuration with Explicit Verification

mmap_size must be applied immediately after opening the connection and before any query warms the cache. Because the kernel may cap the value, always read it back and assert rather than assuming the request succeeded:

import os
import sqlite3
import logging

logger = logging.getLogger(__name__)

def configure_mmap(db_path: str, requested_bytes: int = 268435456) -> sqlite3.Connection:
    """Open a connection with deterministic memory-mapped I/O and verify the map."""
    if not os.path.exists(db_path):
        raise FileNotFoundError(f"Database file not found: {db_path}")

    conn = sqlite3.connect(db_path, timeout=10.0)
    try:
        conn.execute("PRAGMA journal_mode=WAL;")        # concurrent readers; log excluded from mmap
        conn.execute("PRAGMA synchronous=NORMAL;")      # fsync at checkpoint, not every commit
        conn.execute(f"PRAGMA mmap_size={requested_bytes};")  # map up to N bytes of the MAIN db file

        # Read back: the OS/engine may silently cap the value (32-bit space, vm.max_map_count).
        applied = conn.execute("PRAGMA mmap_size;").fetchone()[0]
        if applied != requested_bytes:
            logger.warning(
                "mmap_size capped: requested=%d applied=%d", requested_bytes, applied
            )
        if applied == 0 and requested_bytes > 0:
            # Filesystem (NFS/FUSE/squashfs) may lack mmap semantics — reads fell back to read().
            logger.error("mmap disabled by platform; falling back to cache_size tuning")
        return conn
    except sqlite3.OperationalError:
        conn.close()
        logger.exception("PRAGMA configuration failed")
        raise

The read-back is not defensive boilerplate: a mapping that silently reports back 0 tells you the underlying filesystem rejected mmap(), and you should reallocate that RAM budget to cache_size instead.

Workload Profiles & Threshold Reference

Deployment type Storage Recommended mmap_size Rationale
Embedded eMMC / SD Flash, ≤ 512 MB RAM 67108864 (64 MB) Bounds address-space use; cuts syscall-driven read amplification that wears flash
Desktop NVMe Local SSD, ≥ 8 GB RAM 536870912 (512 MB) Keeps hot indexes and pages resident for sub-ms UI-thread reads
Python automation Batch/ETL host 268435456 (256 MB) Speeds repeated full scans across a pipeline run; pair with a bounded connection pool
High-write IoT Flash gateway 67108864 (64 MB) + tight checkpointing Small map keeps read cost low while WAL stays small under aggressive autocheckpoint

For write-dominated targets, the map size matters less than keeping the WAL small: coordinate with Checkpoint Frequency Tuning and Threshold Tuning for High-Write Workloads so mapped readers are never blocked behind an oversized checkpoint. When the database is reached through pooled or async handles, remember that mapping state is per-connection — see Connection Pooling Strategies and Async Execution Patterns for applying the PRAGMA uniformly across every handle.

Failure Documentation & Edge Cases

Silent value capping

Trigger: A 32-bit process or a kernel with a low vm.max_map_count cannot satisfy the requested map, so SQLite grants a smaller window or none at all. Diagnosis: PRAGMA mmap_size; returns a value below the request, or the applied value is 0. Cross-check the kernel ceiling with cat /proc/sys/vm/max_map_count. Fallback: Log the discrepancy (the code above does this), lower the request to fit the address space, and redirect the unused RAM budget into cache_size per Tuning cache_size for Embedded Linux.

SIGBUS on file truncation

Trigger: A concurrent checkpoint, VACUUM, or bulk schema migration shrinks the database file while a reader is dereferencing a now-truncated mapped page. Diagnosis: The process dies with Bus error (SIGBUS); dmesg shows the faulting address inside the mapped region. Correlate with maintenance windows or VACUUM runs. Fallback: Serialize maintenance behind exclusive access, avoid VACUUM while mapped readers are live, and use PRAGMA wal_autocheckpoint to keep truncation incremental rather than abrupt.

OOM killer activation

Trigger: An aggressive mmap_size on a low-RAM ARM SoC with vm.overcommit_memory permissive enough to accept the reservation, then real memory pressure forces the kernel to reap the process. Diagnosis: dmesg shows Out of memory: Killed process; /proc/<pid>/status shows VmRSS climbing as pages fault in. Compare Mapped vs Cached in /proc/meminfo. Fallback: Cap mmap_size at 25% of physical RAM, prefer cache_size on swap-disabled devices, and never map more than the working set.

Platform lacks mmap semantics

Trigger: The database lives on NFS, a FUSE mount, or a read-only squashfs image where mmap() is unsupported or unsafe. Diagnosis: PRAGMA mmap_size; reports 0 despite a positive request; read latency shows no improvement. Fallback: Treat mapping as unavailable, keep the database on a local ext4/btrfs volume, and lean on cache_size for the read path.

Production Hardening Checklist

Part of the WAL Optimization & Concurrency Tuning series.

Explore this section