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
- 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.
- Renew the lock (Renew) periodically or use LockLeaser, which maintains the lease automatically so the TTL does not lapse before Release.
- Increase the lock TTL in Variable.Lock.TTL if protected work routinely takes longer than the lease.
- Ensure each instance uses its own Acquire before Release; never release a handle that was never successfully acquired.
- 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
- Only call Release on a handle whose Acquire succeeded
- Use LockLeaser to keep the lease renewed so the TTL never lapses before release
- Set a TTL comfortably larger than your worst-case protected work duration
- Treat ErrLockConflict as an expected outcome in multi-instance deployments, not a crash
- Avoid sharing one Locks handle across goroutines or processes
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
- renew 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/3a7eb28475f34d31.
Report an issue: GitHub.