opentofu/opentofu · error

Failed to unlock state: %s

Error message

Failed to unlock state: %s

What it means

After confirmation, force-unlock calls stateMgr.Unlock with the given lock ID. The wrapped error comes from the backend: most commonly the ID does not match the currently held lock (stale or wrong ID), the lock was already released, or the process lacks permission to modify the lock entry (e.g. DynamoDB UpdateItem on the lock table).

Source

Thrown at internal/command/unlock.go:141

			"may still be in use. Only 'yes' will be accepted to confirm."

		v, err := c.UIInput().Input(context.Background(), &tofu.InputOpts{
			Id:          "force-unlock",
			Query:       "Do you really want to force-unlock?",
			Description: desc,
		})
		if err != nil {
			view.Diagnostics(diags.Append(fmt.Errorf("Error asking for confirmation: %s", err)))
			return 1
		}
		if v != "yes" {
			view.ForceUnlockCancelled()
			return 1
		}
	}

	if err := stateMgr.Unlock(context.TODO(), lockID); err != nil {
		view.Diagnostics(diags.Append(fmt.Errorf("Failed to unlock state: %s", err)))
		return 1
	}
	view.ForceUnlockSucceeded()
	return 0
}

func (c *UnlockCommand) Help() string {
	helpText := `
Usage: tofu [global options] force-unlock [options] LOCK_ID

  Manually unlock the state for the defined configuration.

  This will not modify your infrastructure. This command removes the lock on the
  state for the current workspace. The behavior of this lock is dependent
  on the backend being used. Local state files cannot be unlocked by another
  process.

Options:

View on GitHub (pinned to 3561785c48)

Solutions

  1. Get the lock ID from the most recent locking error message, not an older one
  2. Verify you target the same backend and workspace that holds the lock (tofu workspace show)
  3. If the backend reports the lock is already unlocked, nothing is wrong — run a plan to confirm access works
  4. Check write permission on the lock entry (e.g. dynamodb:UpdateItem on the lock table item)
Defensive patterns

Strategy: validation

Validate before calling

// verify the lock is live and its ID matches before unlocking
out, err := exec.Command("tofu", "plan", "-lock=false", "-input=false", "-detailed-exitcode").CombinedOutput()
if err != nil {
	if strings.Contains(string(out), lockID) {
		// lock ID confirmed current: safe to unlock
	} else {
		return errors.New("lock ID stale — take the ID from this latest error instead")
	}
}

Try / catch

if err := stateMgr.Unlock(context.TODO(), lockID); err != nil {
	msg := err.Error()
	switch {
	case strings.Contains(msg, "already unlocked"), strings.Contains(msg, "lock not found"):
		return nil // nothing to do
	case strings.Contains(msg, "does not match"), strings.Contains(msg, "different ID"):
		return fmt.Errorf("stale lock ID — copy the ID from the latest locking error: %s", msg)
	default:
		return err
	}
}

Prevention

When it happens

Trigger: Passing a lock ID from an older failure after the lock was already released or superseded; unlocking a workspace other than the one holding the lock; missing write permission on the lock entry; corrupted lock record in the lock table.

Common situations: Copy-pasting yesterday's lock ID; multiple lock/unlock attempts racing; environments sharing a lock table with mismatched keys.

Related errors


AI-assisted analysis of opentofu/opentofu@3561785c48 (2026-08-15). Data as JSON: /api/errors/1e6c2b547509727c. Report an issue: GitHub.