AdguardTeam/AdGuardHome · error

starting transaction: %w

Error message

starting transaction: %w

What it means

bbolt could not begin a write transaction while loading sessions at startup. The wrapped error typically indicates the database is locked by another holder, was opened read-only, or is in an unusable state.

Source

Thrown at internal/aghuser/sessionstorage.go:152

// newBBoltLogger returns a new [*bbolt.DefaultLogger] that logs messages using
// the given [slog.Logger].  l must not be nil.
func newBBoltLogger(ctx context.Context, l *slog.Logger) (bl *bbolt.DefaultLogger) {
	bl = &bbolt.DefaultLogger{
		Logger: slog.NewLogLogger(l.Handler(), slog.LevelDebug),
	}

	if l.Enabled(ctx, slog.LevelDebug) {
		bl.EnableDebug()
	}

	return bl
}

// loadSessions loads web user sessions from the bbolt database.
func (ds *DefaultSessionStorage) loadSessions(ctx context.Context) (err error) {
	tx, err := ds.db.Begin(true)
	if err != nil {
		return fmt.Errorf("starting transaction: %w", err)
	}

	needRollback := true
	defer func() {
		if needRollback {
			err = errors.WithDeferred(err, tx.Rollback())
		}
	}()

	bkt := tx.Bucket([]byte(bboltBucketSessions))
	if bkt == nil {
		return nil
	}

	removed, err := ds.processSessions(ctx, bkt)
	if err != nil {
		return fmt.Errorf("processing sessions: %w", err)
	}

View on GitHub (pinned to b41aefbe51)

Solutions

  1. Ensure a single process owns the sessions database file
  2. Kill any stale process holding the file lock (lsof /var/lib/app/sessions.db)
  3. If the db was opened read-only intentionally, use a storage implementation that does not write on load

Example fix

# before
instance1 --db /shared/sessions.db &
instance2 --db /shared/sessions.db &
# after
# run one instance, or give each its own sessions.db
Defensive patterns

Strategy: validation

Validate before calling

// ensure exclusive open before constructing storage
if _, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600); err == nil { /* first opener */ } // or use flock

Try / catch

if err := ds.loadSessions(ctx); err != nil {
    if errors.Is(err, bbolt.ErrDatabaseNotOpen) || isLockErr(err) { retry after resolving the lock holder }
}

Prevention

When it happens

Trigger: ds.db.Begin(true) inside loadSessions fails: the bbolt file is flocked by another process, the database was opened with a read-only option, or the file handle is already closed.

Common situations: Two instances of the app sharing one sessions.db; a stale lock from a crashed process that has not been released; opening the same file path twice within the process.

Related errors


AI-assisted analysis of AdguardTeam/AdGuardHome@b41aefbe51 (2026-08-27). Data as JSON: /api/errors/eed03244d6864186. Report an issue: GitHub.