hashicorp/terraform · error

Error unlocking Consul state. Lock ID

Error message

Error unlocking Consul state. Lock ID: %s

Error: %s

You may have to force-unlock this state in order to use it again.
The Consul backend acquires a lock during initialization to ensure
the minimum required key/values are prepared.

What it means

After acquiring the init lock for a non-default workspace, the backend runs RefreshState/WriteState/PersistState inside a lockUnlock helper. If any of those steps fails AND the subsequent stateMgr.Unlock(lockId) also fails, this formatted errStateUnlock is raised and the user is told to force-unlock. It indicates Terraform could not cleanly release the lock it just took.

Solutions

  1. Capture the Lock ID printed in the message and run `terraform force-unlock <LOCK_ID>` (Consul session ID).
  2. Inspect `consul session list` to see whether the session still exists; if gone, the lock will auto-release but force-unlock clears the local view faster.
  3. Verify Consul connectivity (`consul info`, `consul members`) and ACL token validity before retrying.
  4. Re-run the Terraform command once the lock is released.
Defensive patterns

Strategy: try-catch

Try / catch

// After a failed init that may have left a lock, attempt force-unlock once.
sm, diags := backend.StateMgr(name)
if diags.HasErrors() {
    msg := diags.Err().Error()
    if strings.Contains(msg, "force-unlock") {
        if id := extractLockID(msg); id != "" {
            log.Printf("init failed with dangling lock %s; attempting force-unlock", id)
            if _, uerr := sm.Unlock(id); uerr != nil {
                return fmt.Errorf("manual recovery required: %w", uerr)
            }
        }
    }
    return diags.Err()
}

Prevention

When it happens

Trigger: In backend_state.go StateMgr: RefreshState/WriteState/PersistState returns an error; lockUnlock(parent) calls stateMgr.Unlock(lockId) which returns non-nil; the helper returns fmt.Errorf(errStateUnlock, lockId, err).

Common situations: Consul session TTL (15s default) expired while Terraform was still processing; Consul was restarted or lost quorum mid-operation; network dropped between the Terraform host and Consul; the ACL token was revoked during the run.

Related errors


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

Appendix: source

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

	// 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
	if v := stateMgr.State(); v == nil {
		if err := stateMgr.WriteState(states.NewState()); err != nil {
			err = lockUnlock(err)
			return nil, diags.Append(err)
		}
		if err := stateMgr.PersistState(nil); err != nil {

View on GitHub (pinned to d32a084675)