benbjohnson/litestream · error

reacquire read lock: %w

Error message

reacquire read lock: %w

What it means

Immediately after a successful SQLite checkpoint, Litestream re-acquires its read lock (db.acquireReadLock) so ordinary reads/syncs continue to hold it. Failure to reacquire is wrapped with this error and returned from checkpoint(). The checkpoint itself succeeded, but Litestream will not resume normal operation with its read lock held until this succeeds, so the next sync will retry.

Source

Thrown at db.go:2676

	defer func() { _ = db.acquireReadLock(ctx) }()

	// A non-forced checkpoint is issued as "PASSIVE". This will only checkpoint
	// if there are not pending transactions. A forced checkpoint ("RESTART")
	// will wait for pending transactions to end & block new transactions before
	// forcing the checkpoint and restarting the WAL.
	//
	// See: https://www.sqlite.org/pragma.html#pragma_wal_checkpoint
	rawsql := `PRAGMA wal_checkpoint(` + mode + `);`

	var row [3]int
	if err := db.db.QueryRowContext(ctx, rawsql).Scan(&row[0], &row[1], &row[2]); err != nil {
		return 0, err
	}
	db.Logger.Debug("checkpoint", "mode", mode, "result", fmt.Sprintf("%d,%d,%d", row[0], row[1], row[2]))

	// Reacquire the read lock immediately after the checkpoint.
	if err := db.acquireReadLock(ctx); err != nil {
		return 0, fmt.Errorf("reacquire read lock: %w", err)
	}

	return row[1], nil
}

type snapshotReadPosition struct {
	pos          ltx.Pos
	pageSize     int
	walEndOffset int64
	db           *DB
	closeOnce    sync.Once
}

func (p *snapshotReadPosition) close() {
	p.closeOnce.Do(func() { p.db.chkMu.RUnlock() })
}

type snapshotReadCloser struct {

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Check the wrapped error: SQLITE_BUSY means a competing writer — add busy_timeout or shorten application write transactions.
  2. If it is a context deadline issue, increase the checkpoint/sync timeout and retry the sync.
  3. Verify no other process is checkpointing the same database (use replica leasing).
  4. Simply retry; the next sync cycle re-attempts lock acquisition automatically.

Example fix

// before
db.db.Exec("PRAGMA busy_timeout=0")
// after
db.db.Exec("PRAGMA busy_timeout=5000") // tolerate brief writer contention on reacquire
Defensive patterns

Strategy: retry

Try / catch

if err != nil && strings.Contains(err.Error(), "reacquire read lock") {
    // checkpoint succeeded; next sync will retry lock acquisition
    log.Printf("read lock reacquire failed, retrying: %v", err)
}

Prevention

When it happens

Trigger: db.acquireReadLock(ctx) fails right after issuing the SQLite PRAGMA checkpoint: the ctx was canceled/timed out, another writer grabbed the write lock first, or the internal lock promotion (lock-table insert) fails with SQLITE_BUSY.

Common situations: Application holding a long write transaction that blocks the read-lock re-acquire; checkpoint timeout too short for large WALs; process shutting down concurrently.

Related errors


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