hashicorp/terraform · error
invalid lock id: %q. current id: %q
Error message
invalid lock id: %q. current id: %q
What it means
Returned by LocalState.Unlock (local_state.go:217) when the supplied lock id does not match s.lockID. Lock ids are opaque tokens returned by Lock; Unlock requires the same token to prevent one consumer from releasing a lock acquired by another. When the mismatch is detected, Unlock also attempts to read the on-disk lock info so the returned LockError carries diagnostic context about who actually holds the lock.
Source
Thrown at internal/command/clistate/local_state.go:218
}
return "", lockErr
}
s.lockID = info.ID
return s.lockID, s.writeLockInfo(info)
}
func (s *LocalState) Unlock(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.lockID == "" {
return fmt.Errorf("LocalState not locked")
}
if id != s.lockID {
idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID)
info, err := s.lockInfo()
if err != nil {
idErr = errors.Join(idErr, err)
}
return &statemgr.LockError{
Err: idErr,
Info: info,
}
}
os.Remove(s.lockInfoPath())
fileName := s.stateFileOut.Name()
unlockErr := s.unlock()
s.stateFileOut.Close()View on GitHub (pinned to c9def3e214)
Solutions
- Always use the exact id returned by the corresponding Lock call — store and forward it verbatim: `id, _ := s.Lock(info); ...; s.Unlock(id)`.
- Never fabricate or hard-code a lock id; if you lost the id, read it via lockInfo() or force-clear the lock manually rather than guessing.
- When forwarding ids across processes/commands, persist the id in a single source of truth (e.g. a file) and re-read it before Unlock.
- If the mismatch is intentional (force unlock), use the backend's force-unlock subcommand with the id printed by the holder, not an arbitrary value.
Example fix
// before
id, _ := s1.Lock(info)
s2.Unlock(id) // different instance, or stale id -> invalid lock id
// after: one owner, one id
id, err := s.Lock(info)
if err != nil { return err }
defer s.Unlock(id) Defensive patterns
Strategy: validation
Validate before calling
// Validate that the id matches the held lock before unlocking
if id != s.lockID {
return fmt.Errorf("refusing unlock: supplied id %q does not match held id %q", id, s.lockID)
}
return s.Unlock(id) Type guard
// OwnsLock reports whether the given id is the one currently held by this LocalState.
func (s *LocalState) OwnsLock(id string) bool {
s.mu.Lock()
defer s.mu.Unlock()
return id != "" && id == s.lockID
} Prevention
- Forward the exact id returned by Lock; never fabricate one.
- Persist the lock id in a single source of truth when crossing command boundaries.
- Use `terraform force-unlock <id>` only with the printed holder id.
- In wrappers, assert OwnsLock before calling Unlock.
When it happens
Trigger: Passing a hard-coded or stale lock id to Unlock; passing an id from a different LocalState instance; passing an empty string when a real id was returned; a wrapper that stores the wrong id; calling Unlock with the id of a lock acquired by a concurrent process (which is not the same instance).
Common situations: A backend wrapper caches the wrong id after a retry; test code reuses a constant id; a CLI command forwards a lock id from plan to apply but the apply uses a fresh LocalState whose lock has a different id; user-supplied `-lock-id` flag mismatches the actual lock.
Related errors
- LocalState not locked
- state %q already locked
- state file %q locked, but could not unmarshal lock info: %s
- could not write lock info for %q: %s
- failed to lock azure state: %s
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/b35eb2e6baeff5a8.
Report an issue: GitHub.