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
- Check the wrapped error: SQLITE_BUSY means a competing writer — add busy_timeout or shorten application write transactions.
- If it is a context deadline issue, increase the checkpoint/sync timeout and retry the sync.
- Verify no other process is checkpointing the same database (use replica leasing).
- 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
- Set PRAGMA busy_timeout to survive writer contention.
- Keep application write transactions short.
- Increase checkpoint timeouts for large WALs.
- Use leasing to prevent competing Litestream processes.
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.