benbjohnson/litestream · error

lease not held

Error message

lease not held

What it means

ErrLeaseNotHeld signals that a distributed replication lease could not be renewed or released because this instance no longer holds it. In s3/leaser.go it is returned when a conditional (ETag/precondition) write or delete fails — proof that another node took over the lease or it expired.

Source

Thrown at leaser.go:10

package litestream

import (
	"context"
	"errors"
	"fmt"
	"time"
)

var ErrLeaseNotHeld = errors.New("lease not held")

type LeaseExistsError struct {
	Owner     string
	ExpiresAt time.Time
}

func (e *LeaseExistsError) Error() string {
	if e.Owner != "" {
		return fmt.Sprintf("lease already held by %s until %s", e.Owner, e.ExpiresAt.Format(time.RFC3339))
	}
	return fmt.Sprintf("lease already held until %s", e.ExpiresAt.Format(time.RFC3339))
}

type Leaser interface {
	Type() string
	AcquireLease(ctx context.Context) (*Lease, error)
	RenewLease(ctx context.Context, lease *Lease) (*Lease, error)
	ReleaseLease(ctx context.Context, lease *Lease) error

View on GitHub (pinned to 4ed7a308f6)

Solutions

  1. Stop the old replica instance — it lost the lease and must not write
  2. Check for a second litestream process replicating to the same bucket and remove it
  3. Wait for lease expiry if the previous owner crashed, then retry acquisition
  4. Verify system clocks (NTP) on all replicas to avoid premature expiry
Defensive patterns

Strategy: retry

Validate before calling

// before starting a replica, confirm no other instance is replicating:
// check lease object existence/owner via `litestream ltx` or bucket listing

Try / catch

if err := db.SyncAndWait(ctx); err != nil {
    if errors.Is(err, litestream.ErrLeaseNotHeld) {
        // lost the lease: another node took over; stop this replica
        slog.Warn("lease lost, shutting down replication")
        return
    }
    return err
}

Prevention

When it happens

Trigger: RenewLease when the stored ETag no longer matches (another owner acquired the lease), ReleaseLease when isPreconditionFailed on the delete, and s3 acquisition when a LeaseExistsError indicates an already-leased (or expired-lease race) condition that is normalized to ErrLeaseNotHeld.

Common situations: Two litestream replicas pointing at the same S3 bucket where failover occurred and the old primary tries to renew; clock skew causing lease expiry; leftover lease objects from a crashed instance with a stale ETag.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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