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

  1. Treat errors.Is(err, api.ErrLockConflict) as 'lock lost' and re-acquire via Acquire (or stop the protected work immediately).
  2. 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.
  3. Use LockLeaser.Start instead of manual Renew loops so renewal is maintained automatically while the protected function runs.
  4. Ensure only one renewal path exists — don't call Renew manually while LockLeaser is also renewing.
  5. 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

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


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/06904b22372e5b88. Report an issue: GitHub.