benbjohnson/litestream · error
release read lock: %w
Error message
release read lock: %w
What it means
Before issuing a SQLite checkpoint, Litestream must release its long-held read lock so the checkpoint is not starved; it re-acquires the lock via defer. This error wraps a failure of db.releaseReadLock(). Until the read lock is removed, checkpoints cannot make progress, so this failure blocks all WAL truncation/restart operations.
Source
Thrown at db.go:2656
if db.db == nil {
return 0, nil
}
// Track checkpoint metrics.
t := time.Now()
defer func() {
labels := prometheus.Labels{"mode": mode}
db.checkpointNCounterVec.With(labels).Inc()
if err != nil {
db.checkpointErrorNCounterVec.With(labels).Inc()
}
db.checkpointSecondsCounterVec.With(labels).Add(float64(time.Since(t).Seconds()))
}()
// Ensure the read lock has been removed before issuing a checkpoint.
// We defer the re-acquire to ensure it occurs even on an early return.
if err := db.releaseReadLock(); err != nil {
return 0, fmt.Errorf("release read lock: %w", err)
}
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.View on GitHub (pinned to 4ed7a308f6)
Solutions
- Restart the Litestream process to clear in-memory read-lock state and re-establish a balanced acquire/release cycle.
- Look for earlier errors in the log (e.g. a failed re-acquire) that left the lock count unbalanced.
- Avoid calling db internals (acquireReadLock/releaseReadLock) from application code.
- If state persists on disk, run 'litestream reset' for the affected database.
Defensive patterns
Strategy: fallback
Try / catch
if err != nil && strings.Contains(err.Error(), "release read lock") {
// in-memory lock state likely unbalanced: restart litestream process
restartProcess()
} Prevention
- Never call acquireReadLock/releaseReadLock from application code.
- Watch for earlier lock-acquire failures that unbalance the lock count.
- Restart Litestream if checkpoint failures repeat after a prior error.
- Run one Litestream process per database.
When it happens
Trigger: db.releaseReadLock() returns an error at the start of the checkpoint flow: the internal read-lock bookkeeping is inconsistent (acquire/release mismatch), the lock state was corrupted by a prior failed release, or the underlying mechanism to clear the read marker fails.
Common situations: A previous checkpoint attempt errored between acquire and release, leaving the lock count unbalanced; application code calling library internals directly; a crash/restart leaving stale lock state.
Related errors
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/69db4f5f6f07ed5b.
Report an issue: GitHub.