benbjohnson/litestream · error

open wal file: %w

Error message

open wal file: %w

What it means

os.Stat on the database's live WAL file failed while verify was checking whether the LTX WAL offset exceeds the real WAL size. The sync cannot compare LTX state against the WAL if the WAL file cannot be stat'ed.

Source

Thrown at db.go:1712

	if err != nil {
		return info, NewLTXError("open", ltxPath, 0, uint64(exec.pos.TXID), uint64(exec.pos.TXID), err)
	}
	defer func() { _ = ltxFile.Close() }()

	dec := ltx.NewDecoder(ltxFile)
	if err := dec.DecodeHeader(); err != nil {
		// Decode failure indicates corruption
		ltxErr := NewLTXError("decode", ltxPath, 0, uint64(exec.pos.TXID), uint64(exec.pos.TXID), fmt.Errorf("%w: %w", ErrLTXCorrupted, err))
		return info, ltxErr
	}
	info.offset = dec.Header().WALOffset + dec.Header().WALSize
	info.salt1 = dec.Header().WALSalt1
	info.salt2 = dec.Header().WALSalt2
	info.prevCommit = dec.Header().Commit

	// If LTX WAL offset is larger than real WAL then the WAL has been truncated.
	if fi, err := os.Stat(db.WALPath()); err != nil {
		return info, fmt.Errorf("open wal file: %w", err)
	} else if info.offset > fi.Size() {
		exec.state.truncatePassiveFailed = false

		// If we previously synced to the exact end of the WAL, this truncation
		// is expected (normal checkpoint behavior). Reset position and continue
		// incrementally rather than triggering a full snapshot. See issue #927.
		if exec.state.syncedToWALEnd {
			// Read new WAL header to get current salt values
			hdr, err := readWALHeader(db.WALPath())
			if err != nil {
				return info, fmt.Errorf("read wal header after expected truncation: %w", err)
			}

			info.offset = WALHeaderSize
			info.salt1 = binary.BigEndian.Uint32(hdr[16:])
			info.salt2 = binary.BigEndian.Uint32(hdr[20:])
			info.snapshotting = false
			info.reason = ""

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Confirm the -wal file exists next to the database and the path in config matches the actual DB path
  2. Check filesystem permissions for the litestream user
  3. Fix the wrapped inner error (path, permissions, mount) and let the next sync retry
  4. If the database was replaced, restart litestream and reset local state
Defensive patterns

Strategy: validation

Validate before calling

fi, err := os.Stat(dbPath + "-wal")
if err != nil { /* refuse to sync: WAL missing or unreadable */ }

Type guard

func walExists(dbPath string) bool {
    _, err := os.Stat(dbPath + "-wal")
    return err == nil
}

Try / catch

if err := db.Sync(ctx); err != nil {
    if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
        // fix path/permissions, then retry sync
    }
}

Prevention

When it happens

Trigger: During verifyWithExecutor, os.Stat(db.WALPath()) returns an error — WAL file deleted between checkpoint and verify, or permission/IO error on the path.

Common situations: Another process deleted or moved the -wal file; database replaced on disk while litestream holds the old path; directory permissions changed; NFS/network filesystem glitches.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06). Data as JSON: /api/errors/72a7f0679ab6119b. Report an issue: GitHub.