benbjohnson/litestream · warning

timeout waiting for transaction to complete (waited %v)

Error message

timeout waiting for transaction to complete (waited %v)

What it means

SetWriteEnabledWithTimeout(false) was called with timeout > 0 while a SQLite transaction was active, and the transaction did not finish within the timeout. Write support remains enabled (disabling flag rolled back) and the caller receives this error after waiting the full duration.

Source

Thrown at vfs.go:1862

		deadline := time.Now().Add(timeout)
		for f.inTransaction {
			// Check context before waiting
			select {
			case <-f.ctx.Done():
				close(waitDone)
				f.disabling = false
				f.cond.Broadcast() // Wake any waiting Lock() calls
				f.mu.Unlock()
				return fmt.Errorf("context cancelled while waiting for transaction: %w", f.ctx.Err())
			default:
			}
			// Check timeout if specified
			if timeout > 0 && time.Now().After(deadline) {
				close(waitDone)
				f.disabling = false
				f.cond.Broadcast() // Wake any waiting Lock() calls
				f.mu.Unlock()
				return fmt.Errorf("timeout waiting for transaction to complete (waited %v)", timeout)
			}
			f.cond.Wait() // Unlocks mu, waits for signal, relocks mu
		}
		close(waitDone) // Stop the watcher goroutine

		// Sync dirty pages if any exist
		if len(f.dirty) > 0 {
			if err := f.syncToRemoteWithLock(); err != nil {
				f.disabling = false
				f.cond.Broadcast() // Wake any waiting Lock() calls
				f.mu.Unlock()
				return fmt.Errorf("sync before disable: %w", err)
			}
		}

		// Stop sync loop and ticker if running
		if f.syncStop != nil {
			close(f.syncStop)

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Increase the timeout passed to SetWriteEnabledWithTimeout to exceed your worst-case transaction duration.
  2. Find and finish/roll back the leaked transaction (COMMIT or ROLLBACK on the holding connection).
  3. Drain writers (close SQLite connections) before disabling writes so f.inTransaction is false.
  4. Use timeout == 0 (SetWriteEnabled) to wait indefinitely if you must guarantee the disable succeeds.
  5. Instrument transaction duration to detect pathological long transactions.

Example fix

// before
err := file.SetWriteEnabledWithTimeout(false, 2*time.Second) // tx takes 10s
// after
err := file.SetWriteEnabledWithTimeout(false, 60*time.Second) // exceeds max tx time
Defensive patterns

Strategy: try-catch

Validate before calling

// Track transaction duration; only disable when idle:
// if file.InTransaction() { wait or extend timeout }

Try / catch

err := file.SetWriteEnabledWithTimeout(false, maxTxDuration*2)
if err != nil && strings.Contains(err.Error(), "timeout waiting for transaction") {
    // locate leaked transaction, COMMIT/ROLLBACK it, then retry
}

Prevention

When it happens

Trigger: SetWriteEnabledWithTimeout(false, d) invoked while another goroutine holds an open transaction (f.inTransaction true) that runs longer than d — e.g. a large bulk insert, a long-running batch, or a stuck/hung transaction holding the transaction flag.

Common situations: Operational scripts disabling writes on a live database with long transactions and too-short timeouts; a leaked transaction (BEGIN without COMMIT/ROLLBACK) that never completes; contention where the writer is blocked and never signals f.cond.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


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