benbjohnson/litestream · error

begin

Error message

begin: %w

What it means

To take a boundary snapshot at the end of a checkpoint, Litestream begins a transaction (BeginTx) that it will later promote to a write transaction via a lock-table insert. This error wraps a failure to begin that transaction. It means SQLite refused to start the transaction, typically because the database handle is unusable or the connection cannot be obtained.

Solutions

  1. Check the wrapped error: 'context canceled/deadline exceeded' means the checkpoint ctx timed out — increase timeouts or reduce checkpoint frequency.
  2. Ensure the application does not close the *sql.DB while Litestream replication is running.
  3. Raise connection pool limits (db.SetMaxOpenConns) if other queries are starving the pool.
  4. Retry the sync; transient begin failures resolve on the next cycle.

Example fix

// before
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
// after
ctx, cancel := context.WithTimeout(ctx, 30*time.Second) // allow checkpoint + snapshot to finish
Defensive patterns

Strategy: try-catch

Validate before calling

select {
case <-ctx.Done():
    return ctx.Err() // don't start a checkpoint with an already-cancelled context
default:
}

Try / catch

if err != nil && strings.Contains(err.Error(), "begin:") {
    if errors.Is(err, context.DeadlineExceeded) {
        // lengthen timeout and retry
    }
}

Prevention

When it happens

Trigger: db.db.BeginTx(ctx, nil) fails during the checkpoint boundary-snapshot phase: the *sql.DB is closed, the context is already canceled/timed out, or the driver cannot open a connection (too many open connections, connection pool exhausted).

Common situations: Context deadline exceeded because the checkpoint took too long; db.Close() called concurrently by application code; connection pool limits (MaxOpenConns) exhausted by other application queries.

Understand the failure class

Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.

Related errors


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

Appendix: source

Thrown at db.go:2592

	if mode != CheckpointModeTruncate && walFrameN <= preCheckpointFrameN {
		result, err = db.verifyAndSyncWithExecutor(ctx, true, exec, 0)
		if err != nil {
			return false, fmt.Errorf("cannot copy wal after checkpoint: %w", err)
		}
		exec.applySyncResult(result)
		exec.state.syncedSinceCheckpoint = false
		return true, nil
	}

	// Start a transaction. This will be promoted immediately after.
	db.setSyncDiagPhase(diagPhaseCheckpointSnapshotBoundaryLock,
		func(s *diagState) {
			s.checkpointMode = mode
			s.lastSyncedWALOffset = exec.state.lastSyncedWALOffset
		})
	tx, err := db.db.BeginTx(ctx, nil)
	if err != nil {
		return false, fmt.Errorf("begin: %w", err)
	}
	defer func() { _ = rollback(tx) }()

	// Insert into the lock table to promote to a write tx. The lock table
	// insert will never actually occur because our tx will be rolled back,
	// however, it will ensure our tx grabs the write lock. Unfortunately,
	// we can't call "BEGIN IMMEDIATE" as we are already in a transaction.
	if _, err := tx.ExecContext(ctx, `INSERT INTO _litestream_lock (id) VALUES (1);`); err != nil {
		return false, fmt.Errorf("_litestream_lock: %w", err)
	}

	// Copy anything that may have occurred after the checkpoint.
	db.setSyncDiagPhase(diagPhaseCheckpointSnapshotBoundary,
		func(s *diagState) {
			s.checkpointMode = mode
			s.lastSyncedWALOffset = exec.state.lastSyncedWALOffset
		})
	snapshotInfo := syncInfo{

View on GitHub (pinned to 4ed7a308f6)