hashicorp/terraform · error
state already locked
Error message
state %q already locked
What it means
RemoteClient.Lock guards against being called twice on the same client. c.lockCh tracks the in-flight lock: if it is non-nil and the channel has not been drained (default branch of select with lockCh != nil), a lock is already held and Terraform is attempting to re-lock, which violates the locking specification.
Solutions
- Report as a Terraform bug with the stack trace, backend configuration, and Terraform version.
- If you wrap the backend with custom code, ensure you are not invoking Lock twice on the same RemoteClient.
- Restart the Terraform process; a fresh client has lockCh == nil.
Defensive patterns
Strategy: type-guard
Type guard
// isHoldingLock reports whether this RemoteClient already holds a lock.
// (Internal use; stock Terraform should never double-lock.)
func isHoldingLock(c *RemoteClient) bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.lockCh != nil
} Prevention
- Do not reuse a single RemoteClient across overlapping operations.
- If you wrap the backend, never call Lock/Unlock yourself; let Terraform drive it.
- Report any genuine hit as a Terraform bug with a goroutine dump.
- Keep your Terraform build current; locking invariants are tightened over time.
When it happens
Trigger: RemoteClient.Lock(info) invoked when c.lockCh != nil and <-c.lockCh does not return (default branch taken).
Common situations: An internal Terraform bug double-locks the same RemoteClient; a custom caller wrapping the backend invokes Lock directly while Terraform also holds the lock; the client instance was reused across operations. Genuine hits from stock Terraform should be reported as bugs.
Related errors
- Error unlocking Consul state. Lock ID
- error unmarshaling lock info
- expected on 1 response value, got
- failed to lock state in Consul
- consul CAS failed with transaction errors
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/9c091f85eff90b63.
Report an issue: GitHub.
Appendix: 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 d32a084675)