hashicorp/nomad · warning · ErrLockConflict

release conflict %w

Error message

release conflict %w

What it means

This error means the HTTP call to release the lock over a variable returned 409 Conflict, so this client is not the current holder of the lock and cannot release it. The library wraps the sentinel ErrLockConflict so callers can detect lock conflicts with errors.Is. Release intentionally reports this instead of silently succeeding because the server rejected the lock-release operation.

Source

Thrown at api/locks.go:144

// Release makes the call to release the lock over a variable, even if the ttl
// has not yet passed.
// In case of a call to release a non held lock, Release returns ErrLockConflict.
func (l *Locks) Release(ctx context.Context) error {
	var out Variable

	rv := &Variable{
		Lock: &VariableLock{
			ID: l.variable.LockID(),
		},
	}

	_, err := l.c.retryPut(ctx, "/v1/var/"+l.variable.Path+"?lock-release", rv,
		&out, &l.WriteOptions)
	if err != nil {
		callErr, ok := err.(UnexpectedResponseError)

		if ok && callErr.statusCode == http.StatusConflict {
			return fmt.Errorf("release conflict %w", ErrLockConflict)
		}
		return err
	}

	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 {

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Check the lock is still held with errors.Is(err, api.ErrLockConflict) and treat it as expected — another holder owns the lock; skip the release.
  2. Renew the lock (Renew) periodically or use LockLeaser, which maintains the lease automatically so the TTL does not lapse before Release.
  3. Increase the lock TTL in Variable.Lock.TTL if protected work routinely takes longer than the lease.
  4. Ensure each instance uses its own Acquire before Release; never release a handle that was never successfully acquired.
  5. Verify the variable path and lock ID are not shared/overwritten by other writes to the same variable.

Example fix

// before
if err := lock.Release(ctx); err != nil {
    return err
}
// after
if err := lock.Release(ctx); err != nil {
    if errors.Is(err, api.ErrLockConflict) {
        // lock already lost/released elsewhere; not fatal
        return nil
    }
    return err
}
Defensive patterns

Strategy: type-guard

Validate before calling

// before releasing, confirm this handle actually holds the lock
if lock.variable.Lock == nil || lock.variable.LockID() == "" {
    return fmt.Errorf("no lock held on %s", lock.variable.Path)
}

Type guard

func IsLockConflict(err error) bool {
    return errors.Is(err, api.ErrLockConflict)
}

Try / catch

err := lock.Release(ctx)
if err != nil {
    var ure api.UnexpectedResponseError
    switch {
    case errors.Is(err, api.ErrLockConflict):
        // another holder owns the lock; expected in multi-instance setups
    case errors.As(err, &ure):
        // inspect ure.StatusCode for other HTTP failures
    default:
        return err
    }
}

Prevention

When it happens

Trigger: Calling Locks.Release on a lock whose TTL already expired and was taken over by another instance; calling Release twice; calling Release on a Locks handle whose Acquire failed or whose variable.Lock state was overwritten; another process acquired the same variable path between your acquire and release.

Common situations: Multiple replicas of a job all holding Locks handles for the same variable path; long-running work exceeding the lock TTL (DefaultLockTTL 15s) so the lease lapses before Release is called; recreating a Locks client from a stored path without a valid lock ID; clock skew or network partitions delaying renewals.

Related errors


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