{"record":{"id":"c70d6e36937db009","repo":"hashicorp/terraform","slug":"error-unlocking-s3-state-lock-id-s-error-s","errorCode":null,"errorMessage":"Error unlocking S3 state. Lock ID: %s\n\nError: %s\n\nYou may have to force-unlock this state in order to use it again.","messagePattern":"Error unlocking S3 state\\. Lock ID: (.+?)\n\nError: (.+?)\n\nYou may have to force-unlock this state in order to use it again\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/backend/remote-state/s3/backend_state.go","lineNumber":223,"sourceCode":"\t\t\texists = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// We need to create the object so it's listed by States.\n\tif !exists {\n\t\t// take a lock on this state while we write it\n\t\tlockInfo := statemgr.NewLockInfo()\n\t\tlockInfo.Operation = \"init\"\n\t\tlockId, err := client.Lock(lockInfo)\n\t\tif err != nil {\n\t\t\treturn nil, diags.Append(fmt.Errorf(\"failed to lock s3 state: %s\", err))\n\t\t}\n\n\t\t// Local helper function so we can call it multiple places\n\t\tlockUnlock := func(parent error) error {\n\t\t\tif err := stateMgr.Unlock(lockId); err != nil {\n\t\t\t\treturn fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)\n\t\t\t}\n\t\t\treturn parent\n\t\t}\n\n\t\t// Grab the value\n\t\t// This is to ensure that no one beat us to writing a state between\n\t\t// the `exists` check and taking the lock.\n\t\tif err := stateMgr.RefreshState(); err != nil {\n\t\t\terr = lockUnlock(err)\n\t\t\treturn nil, diags.Append(err)\n\t\t}\n\n\t\t// If we have no state, we have to create an empty state\n\t\tif v := stateMgr.State(); v == nil {\n\t\t\tif err := stateMgr.WriteState(states.NewState()); err != nil {\n\t\t\t\terr = lockUnlock(err)\n\t\t\t\treturn nil, diags.Append(err)\n\t\t\t}","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/s3/backend_state.go#L205-L241","documentation":"Thrown by the lockUnlock cleanup helper in s3/backend_state.go:223 when stateMgr.Unlock() fails during the new-workspace init path. The message intentionally includes the Lock ID and tells the user a force-unlock may be required. It fires on the cleanup branch after the main operation errored or finished.","triggerScenarios":"The lock was already released out-of-band; S3 DeleteObject of the .tflock failed (permissions/network); DynamoDB DeleteItem failed; the lock ID no longer matches the stored lock; connection/credentials dropped mid-cleanup.","commonSituations":"Interrupted init where the lock object was partially cleaned; another operator force-unlocked concurrently; IAM role lacks s3:DeleteObject on the lock key or dynamodb:DeleteItem.","solutions":["Run `terraform force-unlock <Lock ID>` with the ID from the message.","Manually delete the stale <key>.tflock S3 object and the DynamoDB LockID row.","Verify the role has s3:DeleteObject and dynamodb:DeleteItem permissions.","Retry init once the lock is cleared."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Verify delete permission on the lock key before init\n// _, err := s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{Bucket:&bucket, Key:aws.String(\"probe\")}) // sandboxed","typeGuard":null,"tryCatchPattern":"// Unlock cleanup failure: warn and instruct force-unlock\n// if err := stateMgr.Unlock(lockID); err != nil {\n//   log.Warn(\"state may still be locked; run terraform force-unlock\", \"id\", lockID, \"err\", err)\n// }","preventionTips":["Grant s3:DeleteObject on the .tflock key and dynamodb:DeleteItem on the lock row.","Don't manually delete lock files out-of-band.","After a crashed run, verify the lock is fully released before the next."],"tags":["s3","locking","force-unlock","cleanup"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}