Chapter 16 · Backup, Export, Recovery, Integrity, and Disaster Preparedness
.backup, Online Backup API, and VACUUM INTO
Create transactionally consistent SQLite snapshots with the CLI backup commands, the online backup API, and VACUUM INTO, then verify and publish backup files safely.
Learning outcomes
A live backup should be created through SQLite rather than by guessing which bytes are stable. SQLite exposes two main snapshot mechanisms: the online backup API, surfaced by many language drivers and the CLI .backup command, and VACUUM INTO, which constructs a compact consistent copy.
Use the CLI .backup/.restore mental model and relate it to the online backup API.
Explain incremental source locking and destination ownership during online backup.
Use VACUUM INTO as a compact live-copy alternative and understand its operational tradeoffs.
Compare cold copy, online backup, and VACUUM INTO without declaring one universally best.
Publish backups through unique temporary names only after verification.
Validate a backup with structural, referential, metadata, and business checks.
The online backup API creates a database snapshot
The core API uses sqlite3_backup_init(), one or more sqlite3_backup_step() calls, and sqlite3_backup_finish(). It copies database pages into a destination database. Incremental stepping lets SQLite release source read locks between chunks so other users can continue, although heavy concurrent writes can force backup work to restart.
sqlite3_backup *b = sqlite3_backup_init(dest, "main", source, "main");if (b != NULL) { do { rc = sqlite3_backup_step(b, 64); /* copy up to 64 pages */ if (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED) sqlite3_sleep(50); } while (rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED); rc_finish = sqlite3_backup_finish(b);}Production code needs complete error handling and retry policy; this compact fragment exists only to reveal the lifecycle. The destination connection must not be used for unrelated work while backup is active.
The CLI exposes the same operational idea
Current SQLite CLI help documents .backup ?DB? FILE, .save as a backup alias, and .restore ?DB? FILE. These are dot-commands interpreted by the shell, not SQL statements that can be prepared through an application driver.
sqlite3 fieldnotes.sqlitesqlite> .backup main fieldnotes-20260812T101700Z.sqlitesqlite> .quit# Verify the backup as a separate database before relying on it.sqlite3 fieldnotes-20260812T101700Z.sqlite "PRAGMA integrity_check; PRAGMA foreign_key_check;"# Restore drill into a disposable target, not blindly over production.sqlite3 restore-drill.sqlitesqlite> .restore main fieldnotes-20260812T101700Z.sqlitesqlite> PRAGMA integrity_check;sqlite> .quitThe course generation environment does not have a standalone sqlite3 executable, so these exact dot-commands are documented from current official CLI help. The equivalent backup behavior was executed through Python’s wrapper around the SQLite backup API.
Python exposes the online backup directly
Python’s standard sqlite3.Connection.backup() is a practical way to see the application form of the same engine feature. The destination is another SQLite connection.
from pathlib import Pathimport os, sqlite3source_path = Path("fieldnotes.sqlite")tmp_path = Path("fieldnotes-20260812T101700Z.sqlite.tmp")final_path = Path("fieldnotes-20260812T101700Z.sqlite")with sqlite3.connect(source_path) as source: dest = sqlite3.connect(tmp_path) try: source.backup(dest, pages=32) finally: dest.close()check = sqlite3.connect(tmp_path)try: assert check.execute("PRAGMA integrity_check").fetchone()[0] == "ok" assert list(check.execute("PRAGMA foreign_key_check")) == [] assert check.execute("PRAGMA application_id").fetchone()[0] == 1179533646 assert check.execute("PRAGMA user_version").fetchone()[0] == 16 assert check.execute("SELECT count(*) FROM maintenance_note").fetchone()[0] == 1200finally: check.close()# Atomic rename on the same filesystem publishes the already-verified file.os.replace(tmp_path, final_path)The temporary name prevents other automation from mistaking an unfinished file for a completed backup. The final rename does not make the backup magically durable on every storage stack; it simply gives consumers a clear publication boundary. Your backup system still needs storage-specific durability and retention controls.
VACUUM INTO: compact copy rather than page-for-page backup
VACUUM INTO, introduced in SQLite 3.27.0, creates a new database containing the same logical content while rebuilding it into a compact form. The source database is unchanged. The destination must not already contain data: the file must be absent or empty.
-- Run outside an active transaction on this connection.SELECT sqlite_version();PRAGMA synchronous;VACUUM INTO 'fieldnotes-compact-20260812T101700Z.sqlite';Current documentation states that the result is a consistent snapshot. It also states that interruption while the command is still running can leave an incomplete/corrupt output. With source synchronous=NORMAL or FULL, current SQLite synchronizes the completed output database to storage before returning, assuming normal OS/filesystem/hardware behavior.
It purges deleted/free content and often produces a smaller file, but it uses more CPU and cannot be stepped incrementally like the backup API. Select it because compact rebuilding is useful, not because every backup must be vacuumed.
Measured compact-copy experiment
The course test created churn data, deleted most of it, then compared the live source file with a VACUUM INTO copy. One run on SQLite 3.46.1 produced the following sizes:
source before VACUUM INTO : 2,179,072 bytescompact destination : 331,776 bytesintegrity_check : okmaintenance_note rows : 1200The important observation is the relationship, not the exact byte count: reusable/free space existed in the source, while the rebuilt destination omitted it. Page size, payloads, indexes, filesystem allocation, and SQLite version can change the numbers.
Compare the physical-copy choices
| Technique | Live source? | Output shape | Operational tradeoff |
|---|---|---|---|
| Cold file copy | No; source must be quiescent | Exact file image at copied state | Very simple/fast, but requires downtime or guaranteed quiescence. |
Online Backup API / .backup | Yes | SQLite database snapshot | Can copy incrementally; usually lower CPU than VACUUM INTO. |
VACUUM INTO | Yes | Rebuilt compact database with same logical content | Can be smaller and purges deleted content; more rebuild work. |
| Filesystem snapshot | Potentially | Storage-level crash image | Depends entirely on atomicity/consistency guarantees of storage stack. |
Verification is part of backup creation
A backup file existing on disk proves only that bytes were written. A useful backup should be opened independently and checked against what the application actually needs.
PRAGMA integrity_check; -- structural / index / constraint checksPRAGMA foreign_key_check; -- referential violations are separatePRAGMA application_id; -- is this the expected application file?PRAGMA user_version; -- what application schema version is it?SELECT count(*) FROM device;SELECT count(*) FROM maintenance_note;SELECT count(*) FROM device WHERE status='active';For a larger system, add invariant queries, expected critical objects, checksums/hashes of stable logical subsets, and an actual application read test. Store the verification result next to backup-job metadata.
Timestamp and naming policy
| Pattern | Why |
|---|---|
fieldnotes-20260812T101700Z.sqlite.tmp | Private/incomplete destination while snapshot and checks are running. |
fieldnotes-20260812T101700Z.sqlite | Published immutable backup after checks pass. |
| UTC timestamp | Avoids DST/local-time ambiguity in distributed operations. |
| Unique destination | Prevents accidental overwrite and makes retention/pruning explicit. |
| Manifest/metadata sidecar | Can record source app version, SQLite version, size, hash, verification status, and policy tier. |
Checkpoint
Choose the snapshot mechanism
Match the requirement to the mechanism.
- You need a consistent live backup while writes continue and want incremental progress.
- You want a compact live snapshot that removes free/deleted content.
- The service can be stopped for a maintenance window and fastest copying matters.
- A backup file exists but has never been opened.
- An operator wants to restore directly over production just to see whether the backup works.
Review the answers
Use the online backup API for the first, VACUUM INTO for the second, and a controlled cold copy for the third. The fourth is unverified and should not be trusted yet. The fifth is a dangerous restore-test design—restore into a disposable target first.
Bridge to logical exports
Physical backups preserve an SQLite database as a database. Lesson 3 changes the artifact completely: .dump emits SQL text that recreates schema and data. That can be excellent for inspection or migration, but it has different size, speed, portability, and extension behavior.