hashicorp/terraform · error
failed to unlock DynamoDB
Error message
failed to unlock DynamoDB: %v
What it means
Dual-lock unlock where the S3 file unlock succeeded but the DynamoDB unlock (unlockWithDynamoDB) failed. The S3 `.tflock` is gone but the DynamoDB LockID row remains, so the state is half-unlocked and other clients may still see the DDB lock. lockErr carries the wrapped DynamoDB failure.
Solutions
- Manually delete the stale DynamoDB row: `aws dynamodb delete-item --table-name <table> --key '{"LockID":{"S":"<bucket>/<path>"}}'`.
- Confirm the table still exists in the configured region/profile: `aws dynamodb describe-table --table-name <table>`.
- Verify dynamodb:GetItem + dynamodb:DeleteItem permissions on the table for the principal.
- If throttling, switch the lock table to on-demand billing or raise write capacity, then retry force-unlock.
- Re-run `terraform force-unlock <id>` once the underlying DDB access is restored.
Example fix
# S3 lock cleared but DynamoDB row lingers — remove it
aws dynamodb delete-item \
--table-name terraform-locks \
--key '{"LockID":{"S":"tf-state-prod/prod/terraform.tfstate"}}' Defensive patterns
Strategy: retry
Validate before calling
// Validate DynamoDB lock-table access before unlock.
func canUnlockDDB(ctx context.Context, c *dynamodb.Client, table, lockID string) error {
if _, err := c.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {
return err
}
return nil
} Try / catch
// On DDB-only unlock failure, retry, then surface the lock ID + row key for manual cleanup.
if derr != nil {
if err2 := c.unlockWithDynamoDB(ctx, id, lockErr); err2 == nil { derr = nil }
}
if derr != nil {
lockErr.Err = fmt.Errorf("failed to unlock DynamoDB: %v; delete row LockID=%s in table %s", derr, lockPath, ddbTable)
return lockErr
} Prevention
- Grant dynamodb:GetItem + dynamodb:DeleteItem on the lock table in the apply role.
- Use on-demand billing or adequate write capacity to avoid throttling during unlock.
- Run `terraform force-unlock <id>` after crashes to clear DDB rows promptly.
- Never delete/recreate the lock table without coordinating with active applies.
When it happens
Trigger: unlockWithDynamoDB returns an error while unlockWithFile succeeded. Triggers: getLockInfoWithDynamoDB fails (GetItem error, table gone, AccessDenied), the DDB lock ID does not match the provided id, or the final DeleteItem on the DDB row fails (throttling, permissions revoked, table deleted between lock and unlock).
Common situations: DynamoDB table deleted or renamed after the lock was taken, IAM principal lost dynamodb:DeleteItem mid-run, provisioned-capacity throttling on the lock table, or a concurrent force-unlock already cleared the DDB row so getLockInfoWithDynamoDB returns no match.
Related errors
- failed to retrieve lock info for lock ID
- failed to unlock S3
- failed to delete the lock file
- failed to unlock both S3 and DynamoDB: S3 error
- lock ID does not match existing lock ( )
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/bfb2cc9fdc189067.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote-state/s3/client.go:495
// Double unlocking: DynamoDB + file
log.Info("Attempting to unlock remote state (S3 Native and DynamoDB)...")
ferr := c.unlockWithFile(ctx, id, lockErr, log)
derr := c.unlockWithDynamoDB(ctx, id, lockErr)
if ferr != nil && derr != nil {
lockErr.Err = fmt.Errorf("failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoDB error: %v", ferr, derr)
return lockErr
}
if ferr != nil {
lockErr.Err = fmt.Errorf("failed to unlock S3: %v", ferr)
return lockErr
}
if derr != nil {
lockErr.Err = fmt.Errorf("failed to unlock DynamoDB: %v", derr)
return lockErr
}
log.Info("Unlocked remote state (S3 Native and DynamoDB)")
return nil
}
// unlockWithFile attempts to unlock the remote state by deleting the lock file from Amazon S3.
//
// This method is used when the S3 native locking mechanism is in use, which uses a `.tflock` file
// to manage state locking. The function deletes the lock file to release the lock, allowing other
// Terraform clients to acquire the lock on the same state file.
func (c *RemoteClient) unlockWithFile(ctx context.Context, id string, lockErr *statemgr.LockError, log hclog.Logger) error {
getInput := &s3.GetObjectInput{
Bucket: aws.String(c.bucketName),
Key: aws.String(c.lockFilePath),
}
View on GitHub (pinned to d32a084675)