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

  1. Report as a Terraform bug with the stack trace, backend configuration, and Terraform version.
  2. If you wrap the backend with custom code, ensure you are not invoking Lock twice on the same RemoteClient.
  3. 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

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


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: " + lockSession

View on GitHub (pinned to d32a084675)