Article ยท part of a guide
Terraform State Lock Errors: Emergency Solutions & Prevention Guide
Fix Terraform state lock errors fast and prevent them: emergency unlock steps, root causes, and best practices to avoid future lockouts.

Key takeaways
- To clear a Terraform state lock, try 'terraform force-unlock' with the Lock ID from the error message first, before resorting to manual DynamoDB deletion or backend APIs.
- Hung or zombie Terraform processes can hold locks, so finding and killing them with tools like ps, pkill, or taskkill is part of recovery.
- Prevent concurrent-run lock errors with CI/CD mutex patterns such as GitLab resource_group or GitHub concurrency groups, and always pass '-lock-timeout' flags.
- Enabling DynamoDB TTL and automated stale-lock monitoring helps locks expire and surface before they block teams.
- Critical safety rules: never force-unlock while another process is actively running, always back up state before manual intervention, and coordinate with your team.
If you're staring at Error acquiring the state lock, the fix is usually terraform force-unlock <ID> with the Lock ID from the error, and it's in section 1 below. Before you run it, check that nobody else is mid-apply. A lock that looks stale is sometimes a slow run, and forcing it open under a live apply is how state gets corrupted. Look at the Who and Created fields in the error, ask in the team channel, then back up the state file. Thirty seconds of checking beats an afternoon of state surgery. Sections 2 and 3 cover hung processes and DynamoDB; 4 to 6 cover stopping this from happening again.
1. How Do You Fix "Error Acquiring the State Lock" With terraform force-unlock?
Terraform Cloud Backend
Error Message:
Error: Error acquiring the state lock
Error message: resource temporarily unavailable
Lock Info:
ID: 12345abc-6789-def0-1234-56789abcdef0
Path: <workspace-name>
Operation: OperationTypePlan
Who: [email protected]
Version: 1.5.0
Created: 2024-11-23 15:30:45 +0000 UTCSolution 1: CLI Force Unlock. Try this first. It asks the backend to release the lock and works whenever the CLI can still reach Terraform Cloud.
# Navigate to your Terraform configuration directory
cd /path/to/terraform/config
# Force unlock using the Lock ID from error message
terraform force-unlock 12345abc-6789-def0-1234-56789abcdef0
# Skip confirmation prompt
terraform force-unlock -force 12345abc-6789-def0-1234-56789abcdef0Solution 2: Terraform Cloud API. Use this when the CLI can't unlock (wrong working directory, missing credentials on the box you're on) but you have an API token.
# Set environment variables
export TOKEN="your-terraform-cloud-token"
export WORKSPACE_ID="ws-ABC123DEF456"
# Force unlock via API
curl \
--header "Authorization: Bearer $TOKEN" \
--header "Content-Type: application/vnd.api+json" \
--request POST \
https://app.terraform.io/api/v2/workspaces/$WORKSPACE_ID/actions/unlockS3 Backend with DynamoDB
Error Message:
Error: Error locking state: Error acquiring the state lock:
ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: terraform-s3-bucket/path/to/terraform.tfstate-md5
Path: terraform-s3-bucket/path/to/terraform.tfstate
Operation: OperationTypePlan
Who: root@runner-123-concurrent-0Solution 1: Terraform CLI Force Unlock. Try this first, with the ID from the error above.
# Must be in terraform directory
cd /path/to/your/terraform/project
# Try CLI unlock first, using the Lock ID from the error message
terraform force-unlock terraform-s3-bucket/path/to/terraform.tfstate-md5Solution 2: Manual DynamoDB Lock Removal. Only if the CLI unlock fails, which usually means the lock row is malformed or the CLI can't reach the backend. This deletes the lock item directly.
# Find your DynamoDB table name
grep -A 10 'backend "s3"' *.tf | grep dynamodb_table
# List current locks
aws dynamodb scan \
--table-name terraform-state-lock \
--region us-east-1
# Delete specific lock
aws dynamodb delete-item \
--table-name terraform-state-lock \
--key '{"LockID": {"S": "your-bucket/path/terraform.tfstate"}}' \
--region us-east-1Emergency Break-Glass Procedure. Last resort, on the machine that held the lock: kill every Terraform process and remove local lock files. Anything those processes were doing is abandoned, so only do this once you're sure they're hung rather than slow.
# Kill all terraform processes
pkill -f terraform
# Remove local lock files
rm -f .terraform/.terraform.tfstate.lock.info
rm -f .terraform.tfstate.lock.info
# Verify no active operations
ps aux | grep terraform2. How Do You Find and Kill Hung Terraform Processes?
Linux/Mac Commands
Find Terraform Processes:
# List all Terraform processes
ps aux | grep terraform | grep -v grep
# Find processes with PIDs
pgrep -fl terraform
# Check for specific state locks
lsof | grep terraform.tfstate
lsof | grep .terraform
# Find zombie processes
ps aux | awk '$8 ~ /^[Zz]/ { print $2 " " $11 }'Kill Hung Processes:
# Safe termination (try first)
kill <PID>
pkill terraform
# Force kill (if normal kill fails)
kill -9 <PID>
pkill -9 terraform
# Kill all terraform processes
pkill -f terraform
pkill -f "terraform apply"
pkill -f "terraform plan"Windows Commands
Find Terraform Processes:
# Command Prompt
tasklist | findstr terraform
tasklist /FI "IMAGENAME eq terraform.exe"
# PowerShell
Get-Process | Where-Object {$_.ProcessName -like "*terraform*"}
Get-Process terraform*Kill Hung Processes:
# Command Prompt
taskkill /PID <PID> /F
taskkill /IM terraform.exe /F
# PowerShell
Stop-Process -Name "terraform" -Force
Stop-Process -Id <PID> -Force
Get-Process terraform* | Stop-Process -ForcePowerShell Script for Long-Running Processes:
# Find and kill terraform processes running > 30 minutes
Get-Process terraform* -ErrorAction SilentlyContinue |
Where-Object {$_.TotalProcessorTime.TotalMinutes -gt 30} |
Stop-Process -Force3. How Do You Investigate and Clean Up DynamoDB Locks?
View Lock Details
Check Lock Table Structure:
# Describe table
aws dynamodb describe-table --table-name terraform-state-lock
# Scan all locks
aws dynamodb scan --table-name terraform-state-lock --region us-east-1
# Pretty print lock information
aws dynamodb scan --table-name terraform-state-lock \
--query 'Items[*].{LockID:LockID.S,Info:Info.S}' \
--output tableFind Specific Locks:
# Search for locks by state path
aws dynamodb scan --table-name terraform-state-lock \
--filter-expression "contains(LockID, :state_path)" \
--expression-attribute-values '{"state_path": {"S": "your-project/terraform.tfstate"}}' \
--region us-east-14. How Do You Prevent Concurrent Terraform Runs From Locking?
Pre-Check Script
#!/bin/bash
# pre-check-lock.sh - Detect existing locks before running
MAX_WAIT_TIME=1800 # 30 minutes
CHECK_INTERVAL=30 # 30 seconds
WAITED_TIME=0
echo "๐ Checking for existing Terraform state locks..."
while [ $WAITED_TIME -lt $MAX_WAIT_TIME ]; do
if timeout 10s terraform plan -lock-timeout=5s -detailed-exitcode >/dev/null 2>&1; then
echo "โ
No lock detected, proceeding"
exit 0
else
echo "โณ State locked, waiting ${CHECK_INTERVAL}s... (${WAITED_TIME}/${MAX_WAIT_TIME}s)"
sleep $CHECK_INTERVAL
WAITED_TIME=$((WAITED_TIME + CHECK_INTERVAL))
fi
done
echo "โ Timeout waiting for state lock"
exit 1Lock Timeout Best Practices
# Always use lock timeouts
terraform plan -lock-timeout=10m
terraform apply -lock-timeout=15m
terraform destroy -lock-timeout=20m
# Enable DynamoDB TTL for auto-expiry
aws dynamodb update-time-to-live \
--table-name terraform-state-lock \
--time-to-live-specification Enabled=true,AttributeName=ExpirationTime \
--region us-east-15. What Do Mutex Patterns Look Like in GitLab and GitHub Actions?
GitLab CI with Resource Groups
# .gitlab-ci.yml - Prevents concurrent Terraform runs
stages:
- validate
- plan
- apply
variables:
TF_IN_AUTOMATION: "true"
# Plan with environment-specific locking
terraform_plan:
stage: plan
resource_group: ${CI_ENVIRONMENT_NAME}_terraform
environment:
name: ${CI_ENVIRONMENT_NAME}
script:
- terraform init
- terraform plan -out=plan.tfplan -lock-timeout=10m
artifacts:
paths:
- plan.tfplan
expire_in: 1 hour
# Apply with strict locking
terraform_apply:
stage: apply
resource_group: ${CI_ENVIRONMENT_NAME}_terraform
environment:
name: ${CI_ENVIRONMENT_NAME}
script:
- terraform apply -input=false plan.tfplan
dependencies:
- terraform_plan
when: manual
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
# Multiple environments with isolated locks
.env_template: &env_template
resource_group: ${ENVIRONMENT}_terraform_deployment
variables:
CI_ENVIRONMENT_NAME: ${ENVIRONMENT}
terraform_dev:
<<: *env_template
variables:
ENVIRONMENT: "dev"
terraform_prod:
<<: *env_template
variables:
ENVIRONMENT: "prod"
when: manualGitHub Actions with Concurrency Groups
Four lines do the work: a workflow-level concurrency group so two pushes to the same ref don't overlap, max-parallel: 1 so environments run one at a time, and a per-job concurrency group with cancel-in-progress: false so a running apply is never cancelled mid-flight.
# Workflow level: one run per ref; cancel superseded PR runs, never main
concurrency:
group: terraform-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
jobs:
apply:
strategy:
matrix:
environment: [dev, staging, prod]
max-parallel: 1 # Sequential execution
concurrency:
group: terraform-apply-${{ matrix.environment }}
cancel-in-progress: false # Never cancel terraform operations
steps:
# ... checkout, setup-terraform, init with per-environment backend key
- run: terraform apply -auto-approve -lock-timeout=15m -var-file="envs/${{ matrix.environment }}.tfvars"Give the plan job the same shape with its own terraform-${{ matrix.environment }} group, and pass -lock-timeout=10m to plan.
6. How Do You Set Up State Lock Monitoring?
CloudWatch Monitoring for DynamoDB
Lambda Function for Lock Age Monitoring:
import boto3
import json
from datetime import datetime, timezone
import os
def lambda_handler(event, context):
dynamodb = boto3.client('dynamodb')
cloudwatch = boto3.client('cloudwatch')
table_name = os.environ['LOCK_TABLE_NAME']
stale_threshold = int(os.environ.get('STALE_THRESHOLD_MINUTES', 30))
# Scan for active locks
response = dynamodb.scan(TableName=table_name)
current_time = datetime.now(timezone.utc)
stale_locks = []
for item in response['Items']:
if 'Info' in item:
lock_info = json.loads(item['Info']['S'])
created_time = datetime.fromisoformat(
lock_info['Created'].replace('Z', '+00:00')
)
lock_age_minutes = (current_time - created_time).total_seconds() / 60
# Send metric
cloudwatch.put_metric_data(
Namespace='Terraform/StateLocks',
MetricData=[{
'MetricName': 'LockAge',
'Value': lock_age_minutes,
'Unit': 'Count'
}]
)
if lock_age_minutes > stale_threshold:
stale_locks.append({
'lock_id': item['LockID']['S'],
'age_minutes': lock_age_minutes,
'who': lock_info.get('Who', 'Unknown')
})
# Send stale lock count
cloudwatch.put_metric_data(
Namespace='Terraform/StateLocks',
MetricData=[{
'MetricName': 'StaleLockCount',
'Value': len(stale_locks),
'Unit': 'Count'
}]
)
return {'statusCode': 200, 'stale_locks': stale_locks}CloudWatch Alarms
# Terraform configuration for alarms
resource "aws_cloudwatch_metric_alarm" "stale_locks" {
alarm_name = "terraform-stale-locks"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "StaleLockCount"
namespace = "Terraform/StateLocks"
period = "300"
statistic = "Maximum"
threshold = "0"
alarm_description = "Terraform state locks are stale"
alarm_actions = [aws_sns_topic.terraform_alerts.arn]
}
resource "aws_cloudwatch_metric_alarm" "long_running_locks" {
alarm_name = "terraform-long-running-locks"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "1"
metric_name = "LockAge"
namespace = "Terraform/StateLocks"
period = "300"
statistic = "Maximum"
threshold = "60" # 60 minutes
alarm_description = "Terraform locks running longer than expected"
alarm_actions = [aws_sns_topic.terraform_alerts.arn]
}Automated Lock Cleanup Script
#!/bin/bash
# Safe automated cleanup with validation
LOCK_TABLE="${LOCK_TABLE:-terraform-state-lock}"
STALE_THRESHOLD_MINUTES="${STALE_THRESHOLD_MINUTES:-60}"
DRY_RUN="${DRY_RUN:-true}"
get_stale_locks() {
aws dynamodb scan \
--table-name "$LOCK_TABLE" \
--output json | \
jq -r --arg threshold "$STALE_THRESHOLD_MINUTES" '
.Items[] |
select(.Info.S != null) |
.Info.S as $info |
($info | fromjson) as $lock_data |
select((now - ($lock_data.Created | strptime("%Y-%m-%dT%H:%M:%S.%fZ") | mktime)) > ($threshold | tonumber * 60)) |
{
lock_id: .LockID.S,
age_minutes: ((now - ($lock_data.Created | strptime("%Y-%m-%dT%H:%M:%S.%fZ") | mktime)) / 60 | floor)
}
'
}
cleanup_stale_lock() {
local lock_id="$1"
if [[ "$DRY_RUN" == "true" ]]; then
echo "DRY RUN: Would delete lock $lock_id"
return 0
fi
aws dynamodb delete-item \
--table-name "$LOCK_TABLE" \
--key "{\"LockID\":{\"S\":\"$lock_id\"}}"
}
# Main execution
echo "Starting lock cleanup (threshold: $STALE_THRESHOLD_MINUTES min)"
get_stale_locks | while read -r lock_info; do
lock_id=$(echo "$lock_info" | jq -r '.lock_id')
cleanup_stale_lock "$lock_id"
doneWhere to start
Fix today's lock with force-unlock and the ID from the error. Then stop the next one: add -lock-timeout (10 to 15 minutes) to every plan and apply, serialise runs in CI with resource_group or concurrency, and put an alarm on lock age so a stuck lock pages someone before it blocks the team.
Most stale-lock fire drills trace back to running your own state backend and locking. Lock hygiene is only one part of keeping runs fast and reliable; the rest is covered in our Terraform optimization guide. A managed platform that owns state and serializes run execution for you means the emergency-unlock steps above rarely come up in the first place. Scalr provides that on usage-based pricing that's free up to 50 runs a month.
Frequently asked questions
How do I fix the Terraform error acquiring the state lock?
Run terraform force-unlock with the Lock ID shown in the error message from your Terraform configuration directory. If that fails on an S3 backend, you can delete the lock item from the DynamoDB table with the AWS CLI, or on Terraform Cloud call the workspace unlock API endpoint. Only do this after confirming no other Terraform process is actually running.
Is it safe to run terraform force-unlock?
It is safe only if no other Terraform process is actively using the state. Never force-unlock while another apply or plan is running, back up your state file before any manual intervention, and coordinate with your team before an emergency unlock. Forcing a lock open under an active run can corrupt state.
How do I prevent Terraform state lock errors in CI/CD pipelines?
Serialize runs so two pipelines never touch the same state at once: use resource_group in GitLab CI or concurrency groups in GitHub Actions, and pass a -lock-timeout flag of 10 to 15 minutes on plan and apply so runs wait instead of failing. Enabling DynamoDB TTL and monitoring lock age also lets stale locks expire or surface before they block the team.
How do I find and kill a hung Terraform process holding a state lock?
On Linux or macOS, list processes with ps aux | grep terraform or pgrep -fl terraform, then terminate with kill or pkill, escalating to kill -9 only if a normal kill fails. On Windows, use tasklist to find terraform.exe and taskkill /F or PowerShell Stop-Process to end it. Once the hung process is gone, retry the unlock.
About the author

CEO at Scalr
Sebastian Stadil is the CEO of Scalr with 15+ years of DevOps experience. He started with AWS in 2004 and advised early Microsoft Azure and Google Cloud.
Part of this guide
9 sheets
Terraform Troubleshooting, Optimization and Error Resolution
- Stop Troubleshooting Terraform: How Scalr AI Helps Platform Teams
- AWS Provider Memory Explosion: The v4.67.0+ Survival Guide
- AWS Provider v6.0: What's Breaking and How to Prepare
- Empty Terraform State File Recovery
- Waiting for 1 run(s) to finish before being queued: Why Terraform Runs Wait
- Terraform Operations at Scale
- Top 5 Best Practices for Terraform