benbjohnson/litestream · error

begin: %w

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.

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)

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.