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) errorView on GitHub (pinned to 4ed7a308f6)
Solutions
- Stop the old replica instance — it lost the lease and must not write
- Check for a second litestream process replicating to the same bucket and remove it
- Wait for lease expiry if the previous owner crashed, then retry acquisition
- 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
- Run NTP on all hosts to prevent premature lease expiry
- Ensure only one replica instance targets the same bucket per DB
- After failover, terminate the old primary before it retries writes
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
- list level %d ltx files: %w
- iterate level %d ltx files: %w
- upload LTX: %w
- %w: expected TXID %d but remote has %d
- sync interval must be greater than 0
AI-assisted analysis of benbjohnson/litestream@4ed7a308f6 (2026-09-06).
Data as JSON: /api/errors/46c4236f998d3b3e.
Report an issue: GitHub.