Terraform State Locked: Fix 'Error locking state' and Access Denied Issues
Fix Terraform state lock errors fast. Step-by-step guide to diagnose and resolve terraform state locked, access denied, and timeout issues with real commands.
- Terraform state locks are created in DynamoDB (AWS), Azure Blob Storage, or GCS to prevent concurrent writes — a stale lock from a crashed process is the most common root cause
- Access denied errors on lock operations usually mean the IAM role or service account lacks s3:PutObject, dynamodb:PutItem, or equivalent permissions on the backend resources
- The fastest safe fix is `terraform force-unlock <LOCK_ID>` after confirming no other process is actively running — never delete the lock record manually without first verifying the ID
- Timeout errors during lock acquisition often signal network instability, DynamoDB throttling, or a misconfigured backend endpoint — check CloudWatch metrics before forcing
- Always run `terraform plan` after unlocking to validate state consistency before applying any changes
| Method | When to Use | Time | Risk |
|---|---|---|---|
| terraform force-unlock <ID> | Confirmed stale lock, no active run | < 1 min | Low — uses official CLI |
| Delete DynamoDB lock item via AWS Console | force-unlock fails due to permissions | 2–5 min | Medium — manual, easy to delete wrong item |
| Fix IAM / service account permissions | Access denied on lock create/release | 5–15 min | Low — additive change |
| Migrate backend to new workspace | State file corrupted or lock record lost | 15–30 min | High — data migration risk |
| Increase DynamoDB provisioned capacity | Throttling causes repeated lock timeouts | 5 min | Low — cost increase only |
| Run terraform init -reconfigure | Backend config changed, init state stale | 2 min | Low — safe re-init |
Understanding the Terraform State Lock Error
Terraform uses a locking mechanism to protect state files from simultaneous writes, which would corrupt shared infrastructure state. When a terraform apply or terraform plan starts, it attempts to acquire a lock on the backend. If the lock cannot be acquired — or if a previous process crashed without releasing it — you will see one of the following errors:
Error: Error locking state: Error acquiring the state lock: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Path: s3://my-bucket/terraform.tfstate
Operation: OperationTypeApply
Who: user@hostname
Version: 1.6.0
Created: 2024-11-12 09:34:21.123456789 +0000 UTC
Info:
Or for access-related failures:
Error: Failed to get existing workspaces: S3 bucket does not exist.
Error: error using credentials to get account ID: operation error STS: GetCallerIdentity,
https response error StatusCode: 403, RequestID: ..., api error AccessDenied: Access denied
Or for permission denied on the lock table:
Error: Error acquiring the state lock: 2 errors occurred:
* ResourceNotFoundException: Requested resource not found
* AccessDeniedException: User: arn:aws:iam::123456789012:user/deploy is not authorized
to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:us-east-1:...
Step 1: Identify the Error Category
Before acting, categorize the failure:
Category A — Stale Lock (process crashed or CI job killed) The lock ID is printed in the error output. No active Terraform process is running. This is the most common scenario after a CI pipeline timeout or a laptop lid-close during apply.
Category B — Access Denied / Permission Denied
The error mentions AccessDeniedException, 403, or permission denied. The process cannot even create or check the lock. This is an IAM or credential issue.
Category C — Timeout / Network
The error says RequestError: send request failed or context deadline exceeded. The process can reach the backend but gets no timely response. This is infrastructure-level.
Category D — Corrupt State or Missing Lock Table
The error mentions ResourceNotFoundException or the state file is malformed. The DynamoDB table or GCS/Azure equivalent does not exist or the state JSON is invalid.
Step 2: Diagnose the Root Cause
For Stale Locks (Category A)
First, confirm no other process holds the lock legitimately:
# Check for running Terraform processes on CI/CD
# In GitHub Actions: check active workflow runs
gh run list --workflow=terraform.yml --status=in_progress
# On AWS, check the DynamoDB lock table directly
aws dynamodb get-item \
--table-name terraform-state-locks \
--key '{"LockID": {"S": "my-bucket/terraform.tfstate"}}' \
--region us-east-1
The response will show WHO created the lock and when. If the timestamp is older than your longest possible apply window (e.g., > 60 minutes) or the WHO field points to a hostname that no longer exists, it is safe to force-unlock.
For Access Denied (Category B)
# Verify current credentials
aws sts get-caller-identity
# Test specific permissions needed for S3 backend + DynamoDB locking
aws s3 ls s3://your-terraform-state-bucket/
aws dynamodb describe-table --table-name terraform-state-locks
# Check what policy is attached
aws iam get-user --user-name deploy-user
aws iam list-attached-user-policies --user-name deploy-user
aws iam list-user-policies --user-name deploy-user
The minimum required IAM permissions for an S3+DynamoDB backend are:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::your-terraform-state-bucket",
"arn:aws:s3:::your-terraform-state-bucket/*"
]
},
{
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/terraform-state-locks"
}
]
}
For Timeouts (Category C)
# Check DynamoDB table metrics in CloudWatch
aws cloudwatch get-metric-statistics \
--namespace AWS/DynamoDB \
--metric-name ThrottledRequests \
--dimensions Name=TableName,Value=terraform-state-locks \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 300 \
--statistics Sum
# Test network connectivity to S3 endpoint
curl -v https://s3.us-east-1.amazonaws.com/ 2>&1 | head -20
Step 3: Apply the Fix
Fix A — Force-Unlock a Stale Lock
# Use the Lock ID from the error output
terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890
Terraform will prompt for confirmation. After unlocking, immediately run:
terraform plan
This confirms state integrity before proceeding.
Fix B — Manual DynamoDB Delete (when force-unlock fails)
# Replace the LockID value with your exact state path
aws dynamodb delete-item \
--table-name terraform-state-locks \
--key '{"LockID": {"S": "your-bucket/path/to/terraform.tfstate"}}' \
--region us-east-1
Warning: Double-check the LockID string exactly matches — deleting the wrong item will have no effect but deleting the wrong table item in a multi-workspace setup can corrupt another team's lock.
Fix C — Resolve Access Denied
Attach the corrected policy:
# Create the policy document
cat > terraform-backend-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": ["s3:GetObject","s3:PutObject","s3:DeleteObject","s3:ListBucket"], "Resource": ["arn:aws:s3:::YOUR_BUCKET","arn:aws:s3:::YOUR_BUCKET/*"]},
{"Effect": "Allow", "Action": ["dynamodb:GetItem","dynamodb:PutItem","dynamodb:DeleteItem"], "Resource": "arn:aws:dynamodb:REGION:ACCOUNT_ID:table/YOUR_TABLE"}
]
}
EOF
aws iam put-user-policy \
--user-name deploy-user \
--policy-name TerraformBackendAccess \
--policy-document file://terraform-backend-policy.json
Fix D — Create Missing DynamoDB Table
If the lock table was accidentally deleted:
aws dynamodb create-table \
--table-name terraform-state-locks \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region us-east-1
Then re-run terraform init to re-register the backend.
Step 4: Prevent Recurrence
- Set pipeline timeouts: In CI/CD, always set a hard timeout and ensure the cleanup step runs
terraform force-unlockwith the lock ID captured at apply-start. - Use workspace isolation: Separate workspaces (or separate state files) per environment prevent a dev lock from blocking production.
- Enable DynamoDB auto-scaling: Prevents throttling-induced lock timeouts at scale.
- Tag lock table with cost allocation tags: Makes it easier to identify ownership and avoid accidental deletion.
- Monitor for stale locks: Set a CloudWatch alarm on DynamoDB items older than N minutes using a Lambda or EventBridge rule.
Frequently Asked Questions
#!/usr/bin/env bash
# terraform-lock-doctor.sh
# Diagnose and optionally fix Terraform state lock issues
# Usage: ./terraform-lock-doctor.sh <s3-bucket> <lock-table> <region> [state-path]
set -euo pipefail
BUCKET="${1:-my-terraform-state}"
TABLE="${2:-terraform-state-locks}"
REGION="${3:-us-east-1}"
STATE_PATH="${4:-terraform.tfstate}"
echo "=== Terraform Lock Doctor ==="
echo "Bucket: $BUCKET | Table: $TABLE | Region: $REGION"
echo ""
# 1. Verify current AWS identity
echo "[1/5] Checking AWS credentials..."
aws sts get-caller-identity --region "$REGION" 2>&1 || {
echo "ERROR: Cannot authenticate. Check AWS_PROFILE, AWS_ACCESS_KEY_ID, or OIDC token."
exit 1
}
# 2. Check S3 bucket access
echo ""
echo "[2/5] Testing S3 bucket access..."
aws s3 ls "s3://${BUCKET}/" --region "$REGION" > /dev/null 2>&1 && \
echo " OK: S3 bucket accessible" || \
echo " FAIL: Cannot list S3 bucket — check s3:ListBucket permission"
# 3. Check DynamoDB table
echo ""
echo "[3/5] Inspecting DynamoDB lock table..."
TABLE_STATUS=$(aws dynamodb describe-table \
--table-name "$TABLE" \
--region "$REGION" \
--query 'Table.TableStatus' \
--output text 2>&1) || TABLE_STATUS="NOT_FOUND"
if [[ "$TABLE_STATUS" == "NOT_FOUND" ]]; then
echo " FAIL: DynamoDB table '$TABLE' not found in $REGION"
echo " Fix: aws dynamodb create-table --table-name $TABLE --attribute-definitions AttributeName=LockID,AttributeType=S --key-schema AttributeName=LockID,KeyType=HASH --billing-mode PAY_PER_REQUEST --region $REGION"
else
echo " OK: Table status = $TABLE_STATUS"
fi
# 4. Scan for existing locks
echo ""
echo "[4/5] Scanning for active locks..."
LOCKS=$(aws dynamodb scan \
--table-name "$TABLE" \
--region "$REGION" \
--output json 2>/dev/null || echo '{"Items":[], "Count":0}')
COUNT=$(echo "$LOCKS" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('Count',0))")
echo " Found $COUNT lock record(s):"
if [[ "$COUNT" -gt 0 ]]; then
echo "$LOCKS" | python3 -c "
import sys, json
d = json.load(sys.stdin)
for item in d.get('Items', []):
lock_id = item.get('LockID', {}).get('S', 'N/A')
info_raw = item.get('Info', {}).get('S', '{}')
try:
info = json.loads(info_raw)
except Exception:
info = {}
print(f\" LockID : {lock_id}\")
print(f\" Who : {info.get('Who', 'unknown')}\")
print(f\" Created: {info.get('Created', 'unknown')}\")
print(f\" Op UUID: {info.get('ID', 'unknown')}\")
print()
"
fi
# 5. Throttling check
echo "[5/5] Checking DynamoDB throttle metrics (last 60 min)..."
THROTTLED=$(aws cloudwatch get-metric-statistics \
--namespace AWS/DynamoDB \
--metric-name ThrottledRequests \
--dimensions Name=TableName,Value="$TABLE" \
--start-time "$(date -u -d '60 minutes ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-60M +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--period 3600 \
--statistics Sum \
--region "$REGION" \
--query 'Datapoints[0].Sum' \
--output text 2>/dev/null || echo "N/A")
echo " ThrottledRequests (1h sum): $THROTTLED"
[[ "$THROTTLED" != "None" && "$THROTTLED" != "N/A" && $(echo "$THROTTLED > 0" | bc -l 2>/dev/null || echo 0) -eq 1 ]] && \
echo " WARNING: Throttling detected — consider switching DynamoDB to PAY_PER_REQUEST billing"
echo ""
echo "=== Diagnosis complete ==="
echo "If a stale lock was found above, run:"
echo " terraform force-unlock <Op UUID from above>"
echo "Then verify state with: terraform plan"
Error Medic Editorial
Error Medic Editorial is a team of senior SREs and DevOps engineers with combined experience across AWS, GCP, Azure, and Kubernetes production environments. Our troubleshooting guides are written from real incident postmortems and reviewed against current upstream documentation.
Sources
- https://developer.hashicorp.com/terraform/language/state/locking
- https://developer.hashicorp.com/terraform/cli/commands/force-unlock
- https://developer.hashicorp.com/terraform/language/backend/s3
- https://github.com/hashicorp/terraform/issues/14447
- https://stackoverflow.com/questions/62894615/terraform-state-lock-in-dynamodb-not-released-after-crash
- https://aws.amazon.com/premiumsupport/knowledge-center/terraform-s3-backend-access-denied/