{"record":{"id":"f4ce1e5d6c2aadb4","repo":"benbjohnson/litestream","slug":"cannot-read-wal-header-w","errorCode":null,"errorMessage":"cannot read wal header: %w","messagePattern":"cannot read wal header: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"db.go","lineNumber":1747,"sourceCode":"\t\t\tinfo.snapshotting = false\n\t\t\tinfo.reason = \"\"\n\t\t\tinfo.clearSyncedToWALEnd = true\n\n\t\t\tdb.Logger.Log(ctx, internal.LevelTrace, \"wal truncated after sync to end (expected checkpoint)\",\n\t\t\t\t\"new_salt1\", info.salt1,\n\t\t\t\t\"new_salt2\", info.salt2)\n\n\t\t\treturn info, nil\n\t\t}\n\n\t\tinfo.reason = \"wal truncated by another process\"\n\t\treturn info, nil\n\t}\n\n\t// Compare WAL headers. Restart from beginning of WAL if different.\n\thdr0, err := readWALHeader(db.WALPath())\n\tif err != nil {\n\t\treturn info, fmt.Errorf(\"cannot read wal header: %w\", err)\n\t}\n\tsalt1 := binary.BigEndian.Uint32(hdr0[16:])\n\tsalt2 := binary.BigEndian.Uint32(hdr0[20:])\n\tsaltMatch := salt1 == dec.Header().WALSalt1 && salt2 == dec.Header().WALSalt2\n\tif !saltMatch {\n\t\texec.state.truncatePassiveFailed = false\n\t}\n\n\t// Handle edge case where we're at WAL header (WALOffset=32, WALSize=0).\n\t// This can happen when an LTX file represents a state at the beginning of the WAL\n\t// with no frames written yet. We must check this before computing prevWALOffset\n\t// to avoid underflow (32 - 4120 = -4088).\n\t// See: https://github.com/benbjohnson/litestream/issues/900\n\tif info.offset == WALHeaderSize {\n\t\tdb.Logger.Debug(\"verify\", \"saltMatch\", saltMatch, \"atWALHeader\", true)\n\t\tif saltMatch {\n\t\t\tinfo.snapshotting = false\n\t\t\treturn info, nil","sourceCodeStart":1729,"sourceCodeEnd":1765,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/db.go#L1729-L1765","documentation":"verify compares the live WAL header salts against the salts recorded in the last LTX file to decide whether the WAL was restarted/rewritten. If the WAL header cannot be read at all, the comparison cannot happen and the sync fails with this error.","triggerScenarios":"readWALHeader(db.WALPath()) returns an error during verify — WAL missing, unreadable, or shorter than the 32-byte WAL header.","commonSituations":"Database checkpointed with delete mode removing the WAL at the exact verify moment; wrong DB path in config; broken mount or permissions.","solutions":["Confirm the WAL file exists and is at least WALHeaderSize (32) bytes","Check litestream has read access to the database directory","Fix the underlying read error surfaced by %w and let sync retry","Ensure no external process (backup scripts, other SQLite tools) is deleting the WAL"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"hdr, err := readWALHeader(dbPath + \"-wal\")\nif err != nil { /* cannot compare salts; fix access first */ }","typeGuard":null,"tryCatchPattern":"if err := db.Sync(ctx); err != nil {\n    if strings.Contains(err.Error(), \"cannot read wal header\") {\n        // check WAL existence/permissions, then backoff-retry\n    }\n}","preventionTips":["Ensure exactly one writer owns the database","Keep DB files on reliable local storage","Verify file sizes: WAL must be >= 32 bytes to be readable"],"tags":["wal","header","salt","read"],"backgroundTag":"file-read-failed","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}