benbjohnson/litestream · error

create _litestream_lock table: %w

Error message

create _litestream_lock table: %w

What it means

Litestream failed to create the `_litestream_lock` table during DB initialization. This table is used to force write locks during sync; sync transactions always roll back so no data remains in it. As with the seq-table error, the underlying driver error is wrapped and carries the real cause.

Source

Thrown at db.go:1092

	// 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)
	}

	// Read page size.
	if err := db.db.QueryRowContext(ctx, `PRAGMA page_size;`).Scan(&db.pageSize); err != nil {
		return fmt.Errorf("read page size: %w", err)
	} else if db.pageSize <= 0 {
		return fmt.Errorf("invalid db page size: %d", db.pageSize)
	}

	// Ensure meta directory structure exists.
	if err := internal.MkdirAll(db.metaPath, db.dirInfo); err != nil {
		return err

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Ensure only one Litestream process monitors each database path.
  2. Check/raise busy_timeout and retry while the conflicting writer finishes.
  3. Verify disk space and write permissions on the database directory.
  4. Run `PRAGMA integrity_check` to rule out corruption; restore from backup if corrupted.

Example fix

// before
# two litestream daemons started (systemd + manual) -> 'database is locked'
// after
systemctl stop litestream; pkill -f 'litestream replicate'; systemctl start litestream
Defensive patterns

Strategy: try-catch

Validate before calling

// ensure single writer: check for competing litestream processes and file writability
fs.accessSync(dbPath, fs.constants.W_OK);

Type guard

function canAcquireWrite(path) { try { return fs.accessSync(path, fs.constants.W_OK) === undefined; } catch { return false; } }

Try / catch

try {
  await db.Open(ctx);
} catch (err) {
  if (String(err.message).includes('create _litestream_lock table')) {
    if (String(err.cause).includes('locked')) { await sleep(backoff); retry(); }
    else throw err;
  }
}

Prevention

When it happens

Trigger: The `CREATE TABLE IF NOT EXISTS _litestream_lock` ExecContext fails during db.init — same class of causes as the seq table: read-only file, SQLITE_BUSY from a concurrent writer, disk full, corrupted database, or the previous seq-table creation succeeded but state changed mid-init.

Common situations: Concurrent litestream processes on the same database racing through init; database locked by a long-running application write transaction; disk quota exceeded right after seq table creation; migrating from an older Litestream version with stale state.

Related errors


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