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

View on GitHub (pinned to c9def3e214)

Solutions

  1. Ensure Lock and Unlock are strictly paired in the calling code; never Lock an already-locked client.
  2. If seen from the stock CLI, report as a terraform bug with the operation sequence.
  3. Restart the terraform process to reset client state and retry the operation.
  4. 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

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


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/9c091f85eff90b63. Report an issue: GitHub.