The thing that clicked today: a backup that completed is not a backup that exists, and two stacked causes behind one empty file are more common than they should be.
On July 9 a backup session had written three of Loupe's databases to the network storage box. On July 24 a routine cleanup of that folder found all three files at zero bytes. Not a truncated database, not even a valid empty-database header. Nothing.
Built / shipped
The root cause, from the kernel log. The storage mount is configured to unmount itself after 60 seconds idle, to keep the network share from being held open all day. A SQLite backup over a network share is written page by page, far slower than a sequential file copy, so about four minutes into the July 9 run the idle timer fired: the kernel logged an unmatched close on the share at 12:35:01 and the automounter tore the mount down at 12:35:44, discarding every unflushed write. Three zero-byte files. Ruled out: no application activity in the failure window (so no lock contention at the app level), and the destination directory existed before the backup calls (so not a missing-parent problem). The backup command itself was confirmed to work correctly against today's live data when written to local disk: 87,777,280 bytes and 62,595,072 bytes for the two main databases, intact.
The obvious fix, correctly blocked. The first build held the mount alive with a keep-alive loop and re-ran the three backup calls straight to the network destination. All three failed instantly with "database is locked." A second, previously unknown property of that mount: it does not support the file-locking calls that SQLite's backup command needs on the destination. The worker stopped, reported both facts, proposed a different approach, and did not improvise.
The approach that worked, after explicit authorization. Back up each database locally first (fast, no locking issue), verify the local copy's size matches the source exactly, plain-copy the finished file to the network destination, verify the size matches again, then run SQLite's own integrity check on each network copy. All three returned ok: 87,777,280, 62,595,072 and 20,480 bytes landed where the zero-byte files had been, and the local temporaries were cleaned up.
Problems & fixes
A fourth zero-byte database, flagged not fixed. The source copy of the smallest database was itself zero bytes on the live machine, a separate pre-existing issue unrelated to the mount. It was filed as its own item for me to decide on rather than quietly "fixed" alongside the others, because silently absorbing an unrelated finding is how a real problem gets papered over.
A harmless side effect, noted. Opening the network copies for the integrity check created empty write-ahead-log sidecar files beside them. Standard, harmless, left in place and recorded so the next person does not wonder.
Decisions
Diagnose from the kernel log, not from the symptom. "Zero bytes" had a timestamp, and the mount's own log explained it to the second.
Stop on a new failure mode. The keep-alive retry was the right first attempt; its instant failure was a new fact, and the worker treated it as one.
Local backup, then copy, then verify size twice and integrity once. The page-by-page backup and the network share do not mix.
Flag unrelated findings; do not fix them in passing.
Learned
A backup's success message is about the process. The only question that matters is whether the file at the destination is the size it should be and opens clean, and nothing in the July 9 run asked it.
Two causes can stack behind one symptom. The idle timeout explained the empty files; it did not predict the locking failure.
An idle-timeout mount and a slow writer are a bad pair. Either write locally and copy, or keep the mount alive for the whole write and make sure the destination supports what the writer needs.
Still open / next
The zero-byte source database is mine to rule on: expected staleness, or restore from the app's own copy. And this is the second companion to the restore-drill lesson from a few weeks later: a backup that reports healthy can still be nothing at all.