hashicorp/terraform · error
state %q already locked
Error message
state %q already locked
What it means
Raised by RemoteClient.Lock() (consul/client.go:382) when Lock() is called while the client already holds an active lock (c.lockCh != nil and not closed). Terraform is designed to lock exactly once per operation, so hitting this is a state-machine violation in the calling code, not a normal user condition.
Source
Thrown at internal/backend/remote-state/consul/client.go:382
defer c.mu.Unlock()
if !c.lockState {
return "", nil
}
c.info = info
// These checks only are to ensure we strictly follow the specification.
// Terraform shouldn't ever re-lock, so provide errors for the 2 possible
// states if this is called.
select {
case <-c.lockCh:
// We had a lock, but lost it.
return "", errors.New("lost consul lock, cannot re-lock")
default:
if c.lockCh != nil {
// we have an active lock already
return "", fmt.Errorf("state %q already locked", c.Path)
}
}
return c.lock()
}
// the lock implementation.
// Only to be called while holding Client.mu
func (c *RemoteClient) lock() (string, error) {
// We create a new session here, so it can be canceled when the lock is
// lost or unlocked.
lockSession, err := c.createSession()
if err != nil {
return "", err
}
// store the session ID for correlation with consul logs
c.info.Info = "consul session: " + lockSessionView on GitHub (pinned to c9def3e214)
Solutions
- Ensure Lock and Unlock are strictly paired in the calling code; never Lock an already-locked client.
- If seen from the stock CLI, report as a terraform bug with the operation sequence.
- Restart the terraform process to reset client state and retry the operation.
- Audit any custom orchestration that drives the backend for a double-Lock path.
Defensive patterns
Strategy: validation
Validate before calling
// Ensure Lock is only called when no lock is held
if c.lockCh != nil {
return nil, errors.New("refusing to lock: client already holds a lock")
} Type guard
// isLocked reports whether the consul RemoteClient currently holds a lock
func isLocked(c *RemoteClient) bool {
if c == nil || c.lockCh == nil { return false }
select {
case <-c.lockCh:
return false // channel closed -> lock lost
default:
return true
}
} Prevention
- Always pair Lock with a deferred Unlock in the same scope.
- Never reuse a RemoteClient across independent operations that each lock.
- Treat a second Lock on the same client as a programming bug.
When it happens
Trigger: Lock() invoked a second time on the same RemoteClient instance without an intervening Unlock(); the default branch in the select sees c.lockCh != nil.
Common situations: A bug in calling code that reuses a RemoteClient and double-locks; a wrapper/orchestrator that calls Lock twice; extremely unusual for stock terraform CLI.
Related errors
- consul CAS failed with transaction errors: %w
- error unmarshaling lock info: %s
- The remote state does not match the expected hash
- expected on 1 response value, got: %d
- consul lock was lost
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/9c091f85eff90b63.
Report an issue: GitHub.