benbjohnson/litestream · error

acquire read lock: %w

Error message

acquire read lock: %w

What it means

Litestream could not start its long-running read transaction (db.acquireReadLock) after creating internal tables. This read transaction prevents other connections from checkpointing the WAL while Litestream observes it. Failure here is a wrapped driver error, most commonly `database is locked`/SQLITE_BUSY or a context cancellation.

Source

Thrown at db.go:1098

		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
	}

	// Ensure WAL has at least one frame in it.
	if err := db.ensureWALExists(ctx); err != nil {
		return fmt.Errorf("ensure wal exists: %w", err)
	}

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Retry opening after the conflicting write/checkpoint completes; stagger Litestream start vs application start.
  2. Remove timeout/short-lived contexts from the Open call so init is not cancelled mid-lock.
  3. Avoid WAL-checkpointing jobs (e.g. `PRAGMA wal_checkpoint(TRUNCATE)` schedulers) during Litestream startup.
  4. Do not place SQLite databases on NFS; use local disk.

Example fix

// before
ctx, cancel := context.WithTimeout(ctx, 50*time.Millisecond)
db.Open(ctx) // init cancelled -> 'acquire read lock: context deadline exceeded'
// after
cancel() // release; call Open with a long-lived context
db.Open(context.Background())
Defensive patterns

Strategy: retry

Validate before calling

// ensure WAL mode and no long-running checkpoint/write jobs at startup
sqlite3 app.db 'PRAGMA journal_mode;'  # expect: wal

Try / catch

async function openWithRetry(db, ctx, attempts = 5) {
  for (let i = 0; i < attempts; i++) {
    try { return await db.Open(ctx); }
    catch (err) {
      if (!String(err.message).includes('acquire read lock') || i === attempts - 1) throw err;
      await sleep(500 * 2 ** i);
    }
  }
}

Prevention

When it happens

Trigger: db.init calls acquireReadLock and the BEGIN/read-transaction cannot be established: another process holds a write lock or is checkpointing, the context passed to Open is cancelled/timed out, or the connection is in a bad state after the earlier CREATE TABLE statements.

Common situations: Starting litestream replicate while a backup job or `VACUUM`/checkpoint is running; application with many writers starving readers; Open called with an already-expired context; databases on network filesystems with flaky locking (NFS).

Understand the failure class

Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.

Related errors


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