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
- Get the lock ID from the most recent locking error message, not an older one
- Verify you target the same backend and workspace that holds the lock (tofu workspace show)
- If the backend reports the lock is already unlocked, nothing is wrong — run a plan to confirm access works
- 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
- Always copy the lock ID from the most recent failed operation's error output
- Confirm workspace and backend match the run that took the lock before unlocking
- Verify write permission on the lock table/entry as part of unlock runbooks
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
- error selecting workspace: %w
- failed to load state manager: %w
- failed to load state: %w
- Failed to refresh state: %s
- Error selecting workspace: %s
AI-assisted analysis of opentofu/opentofu@3561785c48 (2026-08-15).
Data as JSON: /api/errors/1e6c2b547509727c.
Report an issue: GitHub.