hashicorp/nomad · error · ErrLockConflict
renew conflict %w
Error message
renew conflict %w
What it means
This error means the lock-renew call returned HTTP 409 Conflict: the client is no longer the holder of the lock, so the lease cannot be extended. The library wraps ErrLockConflict so callers can detect that they lost the lock during renewal. It typically signals the TTL expired before the renewal reached the server and another holder acquired the lock.
Source
Thrown at api/locks.go:166
return nil
}
// Renew is used to extend the ttl of a lock. It can be used as a heartbeat or a
// lease to maintain the hold over the lock for longer periods or as a sync
// mechanism among multiple instances looking to acquire the same lock.
// Renew will return true if the renewal was successful.
//
// In case of a call to renew a non held lock, Renew returns ErrLockConflict.
func (l *Locks) Renew(ctx context.Context) error {
var out VariableMetadata
_, err := l.c.retryPut(ctx, "/v1/var/"+l.variable.Path+"?lock-renew", l.variable, &out, &l.WriteOptions)
if err != nil {
callErr, ok := err.(UnexpectedResponseError)
if ok && callErr.statusCode == http.StatusConflict {
return fmt.Errorf("renew conflict %w", ErrLockConflict)
}
return err
}
return nil
}
func (l *Locks) LockTTL() time.Duration {
return l.ttl
}
// Locker is the interface that wraps the lock handler. It is used by the lock
// leaser to handle all lock operations.
type Locker interface {
// Acquire will make the actual call to acquire the lock over the variable using
// the ttl in the Locks to create the VariableLock.
//
// Acquire returns the path to the variable holding the lock.View on GitHub (pinned to 482b49bf1a)
Solutions
- Treat errors.Is(err, api.ErrLockConflict) as 'lock lost' and re-acquire via Acquire (or stop the protected work immediately).
- Renew at a fraction of the TTL (e.g. every 0.5-0.7*TTL, as LockLeaser does) instead of near the TTL, or increase the TTL in Variable.Lock.TTL.
- Use LockLeaser.Start instead of manual Renew loops so renewal is maintained automatically while the protected function runs.
- Ensure only one renewal path exists — don't call Renew manually while LockLeaser is also renewing.
- Check network latency/retry settings; slow calls plus retries can push renewals past the TTL.
Example fix
// before
if err := lock.Renew(ctx); err != nil {
log.Fatal(err)
}
// after
if err := lock.Renew(ctx); err != nil {
if errors.Is(err, api.ErrLockConflict) {
// lost the lease: stop work and try to re-acquire
reAcquire(ctx)
return
}
return err
} Defensive patterns
Strategy: retry
Validate before calling
// ensure we hold a lock ID before attempting renewal
if lock.variable.Lock == nil || lock.variable.LockID() == "" {
return fmt.Errorf("cannot renew: no lock held on %s", lock.variable.Path)
}
ttl, err := time.ParseDuration(lock.variable.Lock.TTL)
if err != nil || ttl <= 0 {
return fmt.Errorf("invalid lock TTL %q", lock.variable.Lock.TTL)
} Type guard
func IsLockConflict(err error) bool {
return errors.Is(err, api.ErrLockConflict)
} Try / catch
err := lock.Renew(ctx)
if err != nil {
if errors.Is(err, api.ErrLockConflict) {
// lost the lease: stop protected work and re-acquire
reAcquire(ctx)
return
}
return err
} Prevention
- Renew at a fraction of the TTL (e.g. every 0.5-0.7*TTL, as LockLeaser does), never at or beyond the TTL
- Use LockLeaser instead of hand-rolled renewal loops
- Increase Variable.Lock.TTL if renewals are slow or work is long
- Design for lock loss: on conflict, abort critical-section work immediately
- Monitor network latency to the API server; slow links need larger TTLs
When it happens
Trigger: Calling Locks.Renew after the lock TTL elapsed without a timely renewal; renewing a lock that was already released; two instances alternating Acquire on the same variable path so one's Renew arrives while it is not the holder; Renew on a Locks handle whose lock ID is stale or empty.
Common situations: Renewal period set too close to or beyond the TTL (LockLeaser renews at 0.7*TTL); GC pauses, network latency or retries eating the TTL window; a crashed instance's lock being reclaimed by a peer while the old instance keeps trying to renew; manually driving Renew in a loop with an interval larger than the TTL.
Related errors
- release conflict %w
- lock release: %w
- error renewing the lease: %w
- ErrLockConflict
- failed to load SI token for %s: %w
AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04).
Data as JSON: /api/errors/06904b22372e5b88.
Report an issue: GitHub.