hashicorp/terraform · error
invalid lock id: . current id
Error message
invalid lock id: %q. current id: %q
What it means
Thrown by LocalState.Unlock when the caller-supplied lock ID does not match the ID currently held by this LocalState (s.lockID). Terraform uses the ID to ensure only the lock owner can release it — preventing one operation from unlocking another's lock. On mismatch the lock is left intact and the current holder's ID is reported so the operator can reconcile.
Solutions
- Use the exact lock ID returned by the Lock call that succeeded — do not infer or hard-code.
- If the lock is genuinely stale from a dead process, run `terraform force-unlock <id>` with the reported current id.
- Serialize concurrent runs against the same local state so IDs do not collide.
- Log the lock ID at acquisition so the matching Unlock can be verified.
Defensive patterns
Strategy: validation
Validate before calling
if id != heldID {
return fmt.Errorf("refusing to unlock: provided %q != held %q", id, heldID)
} Prevention
- Thread the exact returned lock ID to the matching Unlock.
- For stale on-disk locks use `terraform force-unlock <id>` rather than Unlock.
- Serialize concurrent Terraform runs against the same local state.
When it happens
Trigger: Passing the wrong ID to Unlock (stale, from a different run, or a different state); concurrent processes each holding their own lock and confusing IDs; an orchestrator that generated/forwarded the wrong ID.
Common situations: Two Terraform processes targeting the same local state; CI retried a job and the second attempt tries to unlock with the first attempt's ID; a wrapper recorded the ID incorrectly.
Related errors
- state already locked
- could not write lock info for
- LocalState not locked
- state file locked, but could not unmarshal lock info
- backendDiags.Err()
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/b35eb2e6baeff5a8.
Report an issue: GitHub.
Appendix: 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 d32a084675)