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.

Beginner115–140 minutesWAL/journal state + cold-copy decision labSQLite 3.53.4 baselineSQLite 3.53.4 baseline · local WAL test on 3.46.1Last reviewed: August 2026

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

01

Distinguish the main database file from rollback-journal, WAL, and shared-memory companion files.

02

Explain cold copies versus live backups in terms of active database state rather than command names.

03

Demonstrate why a main-file-only copy can miss committed WAL transactions.

04

Reason about rollback-journal files without treating them as ordinary backup artifacts.

05

Explain when a filesystem snapshot can be useful and what consistency guarantee it must provide.

06

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.

ArtifactWhat it representsBackup implication
fieldnotes.sqliteMain database pagesSufficient for a cold copy only when SQLite has cleanly settled all required state.
fieldnotes.sqlite-journalRollback-journal recovery state during certain transactionsA hot journal can be required for correct recovery. Do not casually separate/delete it.
fieldnotes.sqlite-walCommitted and uncheckpointed page versions in WAL modeSQLite documents the WAL as part of persistent database state; separating it can lose committed transactions or damage the image.
fieldnotes.sqlite-shmWAL-index shared-memory backing fileUsually 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.

text · cold-copy decision
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 backup
A cold copy can be excellent

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

python · reproduce a main-file-only WAL copy
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.

Do not turn this into “copy DB + WAL manually”

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.

python · observe a rollback journal without corrupting anything
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 propertyWhy it matters
Atomic across relevant filesThe main database and any journal/WAL state must correspond to the same instant.
Crash-consistent semanticsSQLite can normally recover from a valid crash image when required companion state is present.
Documented filesystem behaviorNetwork/distributed filesystems may not provide SQLite locking/WAL assumptions.
Restore validationThe snapshot is not trusted until a restored copy opens and passes integrity and business checks.

Backup decision tree

text · choose the mechanism before the command
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

SymptomLikely misunderstandingFirst response
Backup opens but recent rows are missingCopied only the main file while committed rows were still in WALStop using the copy as authoritative; make a proper live backup from the source.
Found -journal after crashAssuming temporary means disposablePreserve the set and let SQLite recovery logic inspect it.
Copied DB/WAL separately at different timesNo single snapshot boundaryTreat result as suspect; create a new verified backup.
Backup job “succeeded” but restore never testedCopy success confused with recoverabilityPerform 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.

  1. A service is stopped, all connections close cleanly, then the main file is copied.
  2. A WAL-mode service is running; a script copies only fieldnotes.sqlite.
  3. A storage system atomically snapshots the volume containing the database and WAL.
  4. A rollback journal is found after a machine crash and an operator deletes it before opening SQLite.
  5. 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.

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.