Chapter 16 · Backup, Export, Recovery, Integrity, and Disaster Preparedness
Why Copying a Live Database File Can Be Unsafe
Understand which files can contain committed or recovery-critical SQLite state, why a naive live copy can capture the wrong point in time, and when a cold copy is actually safe.
Learning outcomes
A SQLite database looks like one convenient file, which makes ordinary file-copy tools tempting. That mental model is safe only when the database is genuinely quiescent. During normal operation, SQLite may have transaction state in a rollback journal or committed state in a WAL file, so “copy the .sqlite file” is not automatically the same as “capture the database.”
Distinguish the main database file from rollback-journal, WAL, and shared-memory companion files.
Explain cold copies versus live backups in terms of active database state rather than command names.
Demonstrate why a main-file-only copy can miss committed WAL transactions.
Reason about rollback-journal files without treating them as ordinary backup artifacts.
Explain when a filesystem snapshot can be useful and what consistency guarantee it must provide.
Choose a backup technique using a decision tree before running a command.
One database can have several files in play
The main database file stores database pages. In rollback-journal modes, SQLite may temporarily create a journal that preserves original page content while a transaction updates the main file. In WAL mode, committed page versions are appended to the -wal file and later transferred back into the main database by checkpoints. A -shm file normally backs the WAL index used for coordination and fast lookup.
| Artifact | What it represents | Backup implication |
|---|---|---|
fieldnotes.sqlite | Main database pages | Sufficient for a cold copy only when SQLite has cleanly settled all required state. |
fieldnotes.sqlite-journal | Rollback-journal recovery state during certain transactions | A hot journal can be required for correct recovery. Do not casually separate/delete it. |
fieldnotes.sqlite-wal | Committed and uncheckpointed page versions in WAL mode | SQLite documents the WAL as part of persistent database state; separating it can lose committed transactions or damage the image. |
fieldnotes.sqlite-shm | WAL-index shared-memory backing file | Usually reconstructible, but its presence shows that WAL coordination is active; do not invent a file-copy protocol around it. |
Cold copy means quiescent, not merely “the app looks idle”
A cold copy is a filesystem copy made after all processes that can use the database have closed it cleanly, or after a maintenance procedure has otherwise guaranteed quiescence. In normal WAL operation, closing the last connection usually performs a final checkpoint and removes the WAL/SHM files. At that point, copying the main file is straightforward.
STOP or quiesce every process that can open fieldnotes.sqliteVERIFY no database connection remains openVERIFY the expected database file is the one being copiedCOPY fieldnotes.sqlite to a new destinationOPEN the copy with SQLiteRUN integrity + foreign-key + business checksRECORD timestamp/version/application metadataONLY THEN mark the copy as a usable backupA properly controlled cold copy is simple and fast. The mistake is using the same technique while the database is live and assuming the result is transactionally consistent.
Controlled WAL experiment: the main file can be stale
The following disposable experiment creates the schema before WAL mode, disables automatic checkpointing for the demonstration, commits two new rows into the WAL, and copies only the main database file while the connection remains open.
from pathlib import Pathimport shutil, sqlite3, tempfileroot = Path(tempfile.mkdtemp(prefix="wal-copy-demo-"))live = root / "live.sqlite"con = sqlite3.connect(live)con.execute("PRAGMA journal_mode=DELETE")con.execute("CREATE TABLE event(event_id INTEGER PRIMARY KEY, payload TEXT NOT NULL)")con.commit()assert con.execute("PRAGMA journal_mode=WAL").fetchone()[0] == "wal"con.execute("PRAGMA wal_autocheckpoint=0")con.execute("PRAGMA wal_checkpoint(TRUNCATE)")con.execute("INSERT INTO event(payload) VALUES ('wal-1'),('wal-2')")con.commit()print("live rows:", con.execute("SELECT count(*) FROM event").fetchone()[0])print("WAL exists:", Path(str(live) + "-wal").exists())print("SHM exists:", Path(str(live) + "-shm").exists())main_only = root / "main-only.sqlite"shutil.copy2(live, main_only)copy_con = sqlite3.connect(main_only)print("main-only rows:", copy_con.execute("SELECT count(*) FROM event").fetchone()[0])copy_con.close()con.close()In the course verification run, the live connection reported 2 rows while the copied main file reported 0 rows. The copy was readable, but it represented an older state because the committed rows were still in the WAL. That is especially dangerous because a stale backup can look healthy.
SQLite documents that the WAL belongs with the database while active, but sequentially copying a changing set of files is still a race unless an external snapshot mechanism guarantees a single consistent point in time. Prefer an SQLite-aware online backup or a truly atomic storage snapshot.
Rollback-journal mode has a different failure shape
Rollback mode writes original page content to a journal before changing database pages. During an active write transaction, a -journal file can therefore exist even though the transaction is not committed. After a crash, SQLite may need a hot journal to roll the database back to a consistent state.
con = sqlite3.connect("rollback-demo.sqlite")con.execute("PRAGMA journal_mode=DELETE")con.execute("CREATE TABLE IF NOT EXISTS t(id INTEGER PRIMARY KEY, v TEXT)")con.commit()con.execute("BEGIN IMMEDIATE")con.execute("INSERT INTO t(v) VALUES ('not committed')")# Inspect directory now: rollback-demo.sqlite-journal normally exists.con.rollback()con.close()This is an observation lab, not a recipe for copying or deleting journals. If you see a journal beside a database after an abnormal stop, let SQLite open the database and perform its documented recovery instead of manually “cleaning up” files.
Filesystem snapshots can be safe only with the right guarantee
Storage snapshots—from volume managers, filesystems, virtual-machine platforms, or cloud block storage—can be useful when they capture all relevant database state at one atomic point. But “snapshot” is not itself a guarantee: some systems capture each file independently, some require a filesystem freeze, and some are crash-consistent rather than application-consistent.
| Snapshot property | Why it matters |
|---|---|
| Atomic across relevant files | The main database and any journal/WAL state must correspond to the same instant. |
| Crash-consistent semantics | SQLite can normally recover from a valid crash image when required companion state is present. |
| Documented filesystem behavior | Network/distributed filesystems may not provide SQLite locking/WAL assumptions. |
| Restore validation | The snapshot is not trusted until a restored copy opens and passes integrity and business checks. |
Backup decision tree
Is the database completely closed/quiescent? YES -> Cold file copy is simple; verify the copied database. NO -> Is an SQLite-aware live snapshot required? YES -> Online Backup API / CLI .backup OR VACUUM INTO when compact-copy tradeoffs fit. NO -> Does infrastructure provide an atomic crash-consistent snapshot of all database state? YES -> Use only with documented guarantees + restore test. NO -> Do not invent a multi-file cp/copy sequence.Need human-readable / source-controlled SQL instead of a physical snapshot? -> Use .dump, understanding that it is a logical export, not the same artifact.Failure diagnosis before action
| Symptom | Likely misunderstanding | First response |
|---|---|---|
| Backup opens but recent rows are missing | Copied only the main file while committed rows were still in WAL | Stop using the copy as authoritative; make a proper live backup from the source. |
Found -journal after crash | Assuming temporary means disposable | Preserve the set and let SQLite recovery logic inspect it. |
| Copied DB/WAL separately at different times | No single snapshot boundary | Treat result as suspect; create a new verified backup. |
| Backup job “succeeded” but restore never tested | Copy success confused with recoverability | Perform a restore drill and run checks before calling policy complete. |
Checkpoint
Can you identify the backup boundary?
For each scenario, decide whether the database state has a single reliable point in time.
- A service is stopped, all connections close cleanly, then the main file is copied.
- A WAL-mode service is running; a script copies only fieldnotes.sqlite.
- A storage system atomically snapshots the volume containing the database and WAL.
- A rollback journal is found after a machine crash and an operator deletes it before opening SQLite.
- A live database is copied through the SQLite Online Backup API.
Review the answers
The first and fifth are normal safe patterns when performed correctly. The second is unsafe because committed WAL state can be omitted. The third can be valid if the storage guarantee truly covers all relevant state and the restored copy is validated. The fourth is dangerous manual interference with recovery state.
Bridge to SQLite-aware backups
Now that the file-level failure modes are concrete, Lesson 2 introduces mechanisms that ask SQLite itself to construct a consistent snapshot: the CLI .backup/.restore commands, the online backup API that they build upon conceptually, and VACUUM INTO for compact copies.