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.

Beginner120–145 minutesOnline backup + VACUUM INTO verificationSQLite 3.53.4 baselineBackup API + VACUUM INTO 3.27.0+Last reviewed: August 2026

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.

01

Use the CLI .backup/.restore mental model and relate it to the online backup API.

02

Explain incremental source locking and destination ownership during online backup.

03

Use VACUUM INTO as a compact live-copy alternative and understand its operational tradeoffs.

04

Compare cold copy, online backup, and VACUUM INTO without declaring one universally best.

05

Publish backups through unique temporary names only after verification.

06

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.

c · C API shape
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.

text · CLI physical backup and restore
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> .quit
The shell is a client, not the engine

The 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.

python · verified online-backup workflow
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.

sql · compact snapshot
-- 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.

VACUUM INTO is not “better backup” by definition

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:

text · one measured run; your sizes will differ
source before VACUUM INTO : 2,179,072 bytescompact destination       :   331,776 bytesintegrity_check           : okmaintenance_note rows     : 1200

The 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

TechniqueLive source?Output shapeOperational tradeoff
Cold file copyNo; source must be quiescentExact file image at copied stateVery simple/fast, but requires downtime or guaranteed quiescence.
Online Backup API / .backupYesSQLite database snapshotCan copy incrementally; usually lower CPU than VACUUM INTO.
VACUUM INTOYesRebuilt compact database with same logical contentCan be smaller and purges deleted content; more rebuild work.
Filesystem snapshotPotentiallyStorage-level crash imageDepends 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.

sql · minimum verification layers
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

PatternWhy
fieldnotes-20260812T101700Z.sqlite.tmpPrivate/incomplete destination while snapshot and checks are running.
fieldnotes-20260812T101700Z.sqlitePublished immutable backup after checks pass.
UTC timestampAvoids DST/local-time ambiguity in distributed operations.
Unique destinationPrevents accidental overwrite and makes retention/pruning explicit.
Manifest/metadata sidecarCan record source app version, SQLite version, size, hash, verification status, and policy tier.

Checkpoint

Choose the snapshot mechanism

Match the requirement to the mechanism.

  1. You need a consistent live backup while writes continue and want incremental progress.
  2. You want a compact live snapshot that removes free/deleted content.
  3. The service can be stopped for a maintenance window and fastest copying matters.
  4. A backup file exists but has never been opened.
  5. 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.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only assets compatible with the Ethereum/ERC-20 network. Do not send TRC-20/TRON assets.