benbjohnson/litestream · error

enable wal failed, mode=%q

Error message

enable wal failed, mode=%q

What it means

Litestream requires the database to be in WAL journal mode; during init it runs `PRAGMA journal_mode = wal;` and verifies the returned mode is exactly "wal". SQLite accepts the PRAGMA but may keep a different mode (e.g. "delete", "truncate", or returning the old mode when the switch fails), so litestream fails explicitly rather than replicating a non-WAL database it cannot monitor correctly.

Source

Thrown at db.go:1080

	// Ensure database is closed if init fails.
	// Initialization can retry on next sync.
	defer func() {
		if err != nil {
			_ = db.releaseReadLock()
			db.db.Close()
			db.f.Close()
			db.db, db.f = nil, nil
		}
	}()

	// Enable WAL and ensure it is set. New mode should be returned on success:
	// https://www.sqlite.org/pragma.html#pragma_journal_mode
	var mode string
	if err := db.db.QueryRowContext(ctx, `PRAGMA journal_mode = wal;`).Scan(&mode); err != nil {
		return err
	} else if mode != "wal" {
		return fmt.Errorf("enable wal failed, mode=%q", mode)
	}

	// Create a table to force writes to the WAL when empty.
	// There should only ever be one row with id=1.
	if _, err := db.db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS _litestream_seq (id INTEGER PRIMARY KEY, seq INTEGER);`); err != nil {
		return fmt.Errorf("create _litestream_seq table: %w", err)
	}

	// Create a lock table to force write locks during sync.
	// The sync write transaction always rolls back so no data should be in this table.
	if _, err := db.db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS _litestream_lock (id INTEGER);`); err != nil {
		return fmt.Errorf("create _litestream_lock table: %w", err)
	}

	// Start a long-running read transaction to prevent other transactions
	// from checkpointing.
	if err := db.acquireReadLock(ctx); err != nil {
		return fmt.Errorf("acquire read lock: %w", err)

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Ensure litestream (and the db file's directory) has write access so WAL/-shm files can be created; test `sqlite3 /path/to/db 'PRAGMA journal_mode=wal;'` manually
  2. Move the database to a local filesystem that supports WAL (ext4/xfs/apfs/ntfs) instead of NFS/SMB/network shares
  3. Verify the journal mode is persistently WAL: `sqlite3 /path/to/db 'PRAGMA journal_mode;'` — if not, set it once with the app's connections closed
  4. Close other connections/transactions that hold the database and restart litestream (init retries on next sync)

Example fix

-- before (on a db stuck in delete mode)
PRAGMA journal_mode;  -- returns 'delete'
-- after
PRAGMA journal_mode=wal;  -- verify it returns 'wal' before starting litestream
Defensive patterns

Strategy: validation

Validate before calling

// run before starting litestream:
//   sqlite3 /path/to/db 'PRAGMA journal_mode;'
// must print 'wal'. If not:
//   sqlite3 /path/to/db 'PRAGMA journal_mode=wal;'
mode, err := queryScalar(db, "PRAGMA journal_mode")
if err != nil || mode != "wal" { return fmt.Errorf("db not in WAL mode: %q", mode) }

Try / catch

var mode string
if err := db.db.QueryRowContext(ctx, `PRAGMA journal_mode = wal;`).Scan(&mode); err != nil {
    return err
} else if mode != "wal" {
    return fmt.Errorf("enable wal failed, mode=%q", mode) // handle by fixing fs/permissions
}

Prevention

When it happens

Trigger: DB.init: QueryRowContext of `PRAGMA journal_mode = wal;` succeeds but Scan returns a mode string other than "wal". Classic cause: the database is opened in read-only mode or on a filesystem where WAL is unsupported (e.g. some network filesystems like older NFS), or the journal mode switch was rejected because of an active transaction on another connection.

Common situations: Placing the SQLite database on NFS/SMB/network mounts that do not support WAL; the file being opened read-only (read-only filesystem or lack of write permission on the directory needed to create -wal/-shm); another process holding the database in a mode/transaction that blocks the switch; memory-constrained or unusual VFS setups.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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