hashicorp/terraform · error
lock ID ' ' does not match the existing lock ID
Error message
lock ID '%s' does not match the existing lock ID '%s'
What it means
Thrown inside unlockWithFile when the lock file was read and parsed successfully, but the ID field in the file does not equal the id argument passed to Unlock. This is a safety guard: Terraform refuses to delete a lock it does not own, preventing one operator from silently releasing another's lock.
Solutions
- Re-read the current lock ID from the error's 'existing lock ID' value and force-unlock with THAT id: `terraform force-unlock <existing-id>`.
- If you intentionally want to break someone else's lock, confirm with the listed lock holder first, then force-unlock with the existing ID shown in the message.
- Double-check the workspace: ensure you are operating on the same workspace/state path that owns the lock.
- If the existing ID is unknown, fetch the lock file to read it: `aws s3api get-object ... | jq .ID`.
- Avoid running multiple applies concurrently against the same state to prevent ID churn.
Example fix
# before: force-unlock with a stale/wrong ID terraform force-unlock aaaaaaaa-1111-2222-3333-444444444444 # after: use the ID reported in the error's 'existing lock ID' terraform force-unlock bbbbbbbb-9999-8888-7777-666666666666
Defensive patterns
Strategy: validation
Validate before calling
// Before unlock, read the current lock ID from the file and compare.
func currentLockID(ctx context.Context, c *s3.Client, bucket, lockKey string) (string, error) {
out, err := c.GetObject(ctx, &s3.GetObjectInput{Bucket: &bucket, Key: &lockKey})
if err != nil { return "", err }
defer out.Body.Close()
b, err := io.ReadAll(out.Body); if err != nil { return "", err }
var li statemgr.LockInfo
if err := json.Unmarshal(b, &li); err != nil { return "", err }
return li.ID, nil
}
// compare to the id you intend to unlock with; abort if mismatch. Type guard
func isLockOwner(fileID, unlockID string) bool { return fileID != "" && fileID == unlockID } Try / catch
// On mismatch, return the existing ID so the operator can force-unlock correctly.
if lockInfo.ID != id {
return fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'; "+
"run `terraform force-unlock %s` with the existing ID", id, lockInfo.ID, lockInfo.ID)
} Prevention
- Always force-unlock with the ID reported by Terraform, not a previously cached one.
- Do not run concurrent applies against the same workspace.
- Confirm the workspace matches the locked state path before unlocking.
- If unsure who owns the lock, fetch the lock file and read its ID/Operation/Who fields.
When it happens
Trigger: lockInfo.ID != id at client.go:542. Triggers: force-unlock invoked with the wrong ID, two concurrent runs where each holds a different lock ID, a stale lock ID from a previous (already-cleared) run, or the lock file was overwritten by a newer lock holder.
Common situations: Copy-pasting an old lock ID into force-unlock, a teammate acquired a fresh lock after yours timed out, running force-unlock against the wrong workspace's lock file, or the lock file was recreated by another run between when you read the ID and when you unlocked.
Related errors
- lock ID does not match existing lock ( )
- failed to delete the lock file
- failed to read the body of the S3 object
- failed to unlock both S3 and DynamoDB: S3 error
- failed to unlock S3
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/810af0e432a5ac46.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote-state/s3/client.go:543
if cerr := getOutput.Body.Close(); cerr != nil {
log.Warn(fmt.Sprintf("failed to close S3 object body: %v", cerr))
}
}()
data, err := io.ReadAll(getOutput.Body)
if err != nil {
return fmt.Errorf("failed to read the body of the S3 object: %w", err)
}
lockInfo := &statemgr.LockInfo{}
if err := json.Unmarshal(data, lockInfo); err != nil {
return fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err)
}
lockErr.Info = lockInfo
// Verify that the provided lock ID matches the lock ID of the retrieved lock file.
if lockInfo.ID != id {
return fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'", id, lockInfo.ID)
}
// Delete the lock file to release the lock.
_, err = c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(c.bucketName),
Key: aws.String(c.lockFilePath),
})
if err != nil {
return fmt.Errorf("failed to delete the lock file: %w", err)
}
log.Debug(fmt.Sprintf("Deleted lock file: '%q'", c.lockFilePath))
return nil
}
func (c *RemoteClient) unlockWithDynamoDB(ctx context.Context, id string, lockErr *statemgr.LockError) error {View on GitHub (pinned to d32a084675)