hashicorp/terraform · error

failed to lock state in Consul

Error message

failed to lock state in Consul: %s

What it means

Thrown by the Consul backend's StateMgr during initialization of a non-default workspace. The backend acquires a Consul lock so it can atomically write an empty sentinel state (so States() can later discover the workspace). If stateMgr.Lock returns an error, it is wrapped here. The underlying cause is typically that another client holds the lock, the Consul agent is unreachable, or the ACL token cannot create a session or write the lock key.

Solutions

  1. Read the wrapped error: if it names a lock holder / lock ID, run `terraform force-unlock <LOCK_ID>` (the ID is the Consul session ID).
  2. Check Consul health and connectivity from the Terraform host: `consul members`, `consul kv get -recurse <state-path>`.
  3. Verify the backend's ACL token grants session:write and kv:write on the state prefix; recreate the token if revoked.
  4. Coordinate with the team so only one run targets the workspace at a time; re-run terraform init/apply once the lock is cleared.
Defensive patterns

Strategy: retry

Validate before calling

// Pre-check whether a Consul lock is already held for the state path
// before attempting StateMgr init.
func consulLockHeld(client *consulapi.Client, statePath string) (bool, error) {
    pair, _, err := client.KV().Get(strings.TrimRight(statePath, "/")+".lock", nil)
    if err != nil {
        return false, err
    }
    return pair != nil, nil
}

// if held, surface a clear message instead of letting Lock() fail opaquely.

Try / catch

// Treat statemgr.LockError specially: surface the lock ID so the user can force-unlock.
sm, diags := backend.StateMgr(name)
if diags.HasErrors() {
    var le *statemgr.LockError
    for _, d := range diags {
        if errors.As(d.Err(), &le) && le.Info != nil {
            return fmt.Errorf("state %q is locked (id=%s); run terraform force-unlock %s", name, le.Info.ID, le.Info.ID)
        }
    }
    return diags.Err()
}

Prevention

When it happens

Trigger: backend.StateMgr(name) with name != backend.DefaultStateName -> stateMgr.Lock(lockInfo) returns a non-nil error. The lock fails inside RemoteClient.lock() when createSession() or consulLock.Lock(...) errors out (e.g. another session holds the key, LockWaitTime elapsed after LockTryOnce, ACL denied, network error).

Common situations: A previous Terraform run crashed without releasing its Consul session; a teammate is running terraform apply against the same workspace; the Consul ACL token used by the backend lacks session:write or kv:write on the state prefix; the Consul agent is down or partitioned; the session TTL expired mid-run.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/e2303acc2aef9765. Report an issue: GitHub.

Appendix: source

Thrown at internal/backend/remote-state/consul/backend_state.go:108

	}

	if !b.lock {
		stateMgr.DisableLocks()
	}

	// the default state always exists
	if name == backend.DefaultStateName {
		return stateMgr, nil
	}

	// Grab a lock, we use this to write an empty state if one doesn't
	// exist already. We have to write an empty state as a sentinel value
	// so States() knows it exists.
	lockInfo := statemgr.NewLockInfo()
	lockInfo.Operation = "init"
	lockId, err := stateMgr.Lock(lockInfo)
	if err != nil {
		return nil, diags.Append(fmt.Errorf("failed to lock state in Consul: %s", err))
	}

	// Local helper function so we can call it multiple places
	lockUnlock := func(parent error) error {
		if err := stateMgr.Unlock(lockId); err != nil {
			return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
		}

		return parent
	}

	// Grab the value
	if err := stateMgr.RefreshState(); err != nil {
		err = lockUnlock(err)
		return nil, diags.Append(err)
	}

	// If we have no state, we have to create an empty state

View on GitHub (pinned to d32a084675)