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
- Increase the timeout passed to SetWriteEnabledWithTimeout to exceed your worst-case transaction duration.
- Find and finish/roll back the leaked transaction (COMMIT or ROLLBACK on the holding connection).
- Drain writers (close SQLite connections) before disabling writes so f.inTransaction is false.
- Use timeout == 0 (SetWriteEnabled) to wait indefinitely if you must guarantee the disable succeeds.
- 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
- Size the disable timeout above your worst-case transaction duration.
- Always COMMIT/ROLLBACK — audit for leaked BEGIN without end.
- Drain or close writer connections before disabling writes.
- Alert on long-running transactions (> p99 duration).
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.
- Timeouts: ETIMEDOUT, deadlines, and hung requests — what actually expires when a request times out.
Related errors
- download snapshot: %w
- write to buffer: %w
- page size mismatch: got %d, expected %d
- context cancelled while waiting for transaction: %w
- sync before disable: %w
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/4e2abe5bb9ec3075.
Report an issue: GitHub.