TrademarkTrademark
Features
Documentation
  1. Learning Center
  2. Terraform Troubleshooting, Optimization and Error Resolution

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.

Terraform State Lock Errors: Emergency Solutions & Prevention Guide

Key takeaways

  1. 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.
  2. Hung or zombie Terraform processes can hold locks, so finding and killing them with tools like ps, pkill, or taskkill is part of recovery.
  3. 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.
  4. Enabling DynamoDB TTL and automated stale-lock monitoring helps locks expire and surface before they block teams.
  5. 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 UTC

Solution 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-56789abcdef0

Solution 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/unlock

S3 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-0

Solution 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-md5

Solution 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-1

Emergency 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 terraform

2. 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 -Force

PowerShell 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 -Force

3. 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 table

Find 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-1

4. 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 1

Lock 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-1

5. 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: manual

GitHub 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"
done

Where 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

Sebastian Stadil

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

8 articles