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

  1. Restart the Litestream process to clear in-memory read-lock state and re-establish a balanced acquire/release cycle.
  2. Look for earlier errors in the log (e.g. a failed re-acquire) that left the lock count unbalanced.
  3. Avoid calling db internals (acquireReadLock/releaseReadLock) from application code.
  4. 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

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.