Skip to content

Fix: Terraform Error locking state: Error acquiring the state lock

FixDevs · (Updated: )

Part of:  Docker, DevOps & Infrastructure

Quick Answer

How to fix Terraform state lock error caused by concurrent runs, crashed operations, DynamoDB lock table issues, and stale lock IDs.

The Error

You run terraform plan or terraform apply and get:

Error: Error locking state: Error acquiring the state lock

Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
  ID:        abcd1234-ef56-7890-abcd-ef1234567890
  Path:      my-project/terraform.tfstate
  Operation: OperationTypeApply
  Who:       user@hostname
  Version:   1.7.0
  Created:   2024-01-15 10:30:00.000000000 +0000 UTC
  Info:

Terraform acquires a state lock to protect against two processes writing
to the state at the same time. This error means Terraform could not
acquire the lock.

Terraform cannot acquire the state lock because another process is already holding it. The lock prevents concurrent state modifications that could corrupt the state file.

Why This Happens

Terraform uses state locking to ensure only one person or process modifies infrastructure at a time. When using a remote backend (S3 + DynamoDB, Terraform Cloud, GCS, etc.), a lock entry is created before any state-modifying operation.

The lock entry contains identifying information: a UUID lock ID, the operation type (plan / apply / destroy), the user, the hostname, the Terraform version, and a timestamp. When a Terraform process exits cleanly — successful apply, graceful failure, or Ctrl+C during a stage that can be cancelled — it releases the lock. When the process dies without that release path running (kill -9, instance terminated, network drop after the conditional write succeeded), the lock is stranded: still recorded as held, but with no live owner to release it.

This is intentional. Terraform errs on the side of preserving the lock because the alternative — automatic lock expiry — risks two writers stepping on each other and corrupting state. The cost of a stranded lock is a deploy freeze. The cost of corrupt state is a multi-hour reconstruction. The product chooses the cheaper failure mode and asks you to resolve stranded locks explicitly.

Common causes:

  • Another terraform apply is running. A teammate or CI/CD pipeline is actively applying changes.
  • Previous operation crashed. A terraform apply was interrupted (Ctrl+C, network failure, timeout) and the lock was not released.
  • CI/CD pipeline timeout. A pipeline ran terraform apply but timed out before completion, leaving the lock.
  • DynamoDB table issue. The lock table does not exist, has wrong permissions, or the lock entry is stale.
  • Terraform Cloud workspace locked. Someone locked the workspace manually or a run is queued.

In Production: Incident Lens

In production, a stranded lock surfaces as all infra changes frozen for that workspace. Every CI run on every branch that touches Terraform fails at terraform plan with the same Error acquiring the state lock message. Engineers cannot merge PRs that depend on a successful plan check. Hotfixes that need infrastructure changes — bumping a security group rule, scaling an Auto Scaling Group, rotating a secret — are blocked until someone reconciles the lock.

The blast radius is one workspace, but the workspace can own a critical surface: production VPCs, prod IAM, or the cluster that hosts all your services. A locked production state during an active incident is the worst possible time to discover that nobody on call knows how to safely force-unlock. The teammate listed in Who: may be asleep or on vacation. The CI job ID in Info: may be from a pipeline that was rerun three times since.

The monitoring signal to set up: alert on consecutive Terraform plan failures in CI within the same workspace. Many teams pipe terraform plan exit codes to Datadog or Honeycomb. If three plans in a row fail with exit code 1 containing “Error acquiring the state lock,” page on-call. The same signal applies in scheduled drift-detection runs — if drift detection has been failing silently for 24 hours because of a stale lock, you have lost a day of drift visibility.

Recovery sequence: read the lock metadata. Verify Who: is not actively running anything (ask in chat, check CI dashboards). If the lock is genuinely stale, run terraform force-unlock <ID>. If force-unlock itself fails (typically because the backend table moved or permissions changed), delete the lock entry from DynamoDB directly with aws dynamodb delete-item. Then re-run terraform plan and verify state is intact before applying.

Postmortem preventive: the structural fix is shorter CI timeouts and PR-scoped state locks. Use -lock-timeout=5m on every apply so transient races resolve themselves. Cap CI Terraform jobs at well under your backend’s lock timeout (15 minutes is a sane default for most pipelines). Use GitHub Actions concurrency groups keyed on the workspace name so two PRs touching the same workspace queue up instead of racing. Add a cleanup step that runs terraform force-unlock against a recorded lock ID on job failure, but only if the lock owner matches the current CI run.

Fix 1: Wait for the Other Operation

Check if another operation is genuinely running. The error shows Who: (the user) and Created: (when the lock was acquired).

If the lock was created minutes ago by a teammate, wait for their operation to finish:

# Check with your team
# Or look at CI/CD pipeline status

If the lock is recent and the Who: field shows a valid user or CI system, do not force-unlock — wait for the operation to complete.

Fix 2: Force Unlock a Stale Lock

If the lock is stale (the operation crashed, the CI pipeline timed out, or the user confirmed they are not running anything):

terraform force-unlock LOCK_ID

Use the Lock ID from the error message:

terraform force-unlock abcd1234-ef56-7890-abcd-ef1234567890

Terraform asks for confirmation:

Do you really want to force-unlock?
  Terraform will remove the lock on the remote state.
  This will allow local Terraform commands to modify this state, even though it
  may still be in use. Only 'yes' will be accepted to confirm.

Type yes to confirm.

Warning: Only force-unlock if you are certain no other operation is running. Force-unlocking while another process is actively modifying state can corrupt the state file.

Pro Tip: Before force-unlocking, check if the user listed in Who: is still running Terraform. Ask them directly. If the lock was created by a CI/CD system, check the pipeline status. A stale lock from a crashed pipeline is safe to force-unlock; a lock from an in-progress pipeline is not.

Fix 3: Fix DynamoDB Lock Table (AWS S3 Backend)

For S3 backends, the lock is stored in a DynamoDB table. If the table does not exist or is misconfigured:

Create the lock table:

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Backend configuration:

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "project/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Check the table exists and has the right schema:

aws dynamodb describe-table --table-name terraform-locks

The table must have a string hash key named LockID.

Check DynamoDB permissions:

Your IAM user or role needs dynamodb:GetItem, dynamodb:PutItem, dynamodb:DeleteItem on the lock table:

{
  "Effect": "Allow",
  "Action": [
    "dynamodb:GetItem",
    "dynamodb:PutItem",
    "dynamodb:DeleteItem"
  ],
  "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/terraform-locks"
}

For IAM permission issues in general, see Fix: AWS AccessDeniedException.

Fix 4: Manually Remove a Stale DynamoDB Lock

If terraform force-unlock does not work, remove the lock entry directly from DynamoDB:

aws dynamodb delete-item \
  --table-name terraform-locks \
  --key '{"LockID": {"S": "my-project/terraform.tfstate"}}'

Or use the DynamoDB console:

  1. Go to DynamoDB → Tables → terraform-locks → Explore table items
  2. Find the item with LockID matching your state file path
  3. Delete the item

Then retry your Terraform command.

Warning: Directly deleting DynamoDB items bypasses Terraform’s safety checks. Only do this when terraform force-unlock fails.

Fix 5: Fix Terraform Cloud Locks

If using Terraform Cloud or Terraform Enterprise:

Check the workspace:

  1. Go to Terraform Cloud → Workspaces → Your workspace
  2. Check for running or queued plans/applies
  3. If a run is stuck, click “Discard run”

Unlock the workspace:

  1. Go to Workspace → Settings → General
  2. Click “Force Unlock” (requires admin permissions)

Via API:

curl -s \
  --header "Authorization: Bearer $TFC_TOKEN" \
  --header "Content-Type: application/vnd.api+json" \
  --request POST \
  "https://app.terraform.io/api/v2/workspaces/$WORKSPACE_ID/actions/force-unlock"

Common Mistake: Running terraform apply locally when the workspace is configured for remote execution. The local run acquires a lock that conflicts with the remote execution. Use terraform cloud for remote execution or configure execution_mode = "local" in the workspace settings.

Fix 6: Prevent Lock Issues in CI/CD

Configure your CI/CD pipeline to handle locks gracefully:

GitHub Actions:

jobs:
  terraform:
    runs-on: ubuntu-latest
    concurrency:
      group: terraform-${{ github.ref }}
      cancel-in-progress: false  # Don't cancel — let it finish
    steps:
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform apply -auto-approve
        timeout-minutes: 30

concurrency ensures only one Terraform job runs at a time per branch.

Add timeouts:

- run: terraform apply -auto-approve
  timeout-minutes: 30

This kills the job after 30 minutes, but the lock might still be held. Add cleanup:

- run: terraform apply -auto-approve
  timeout-minutes: 30
  continue-on-error: true

- run: terraform force-unlock -force $LOCK_ID || true
  if: failure()

Use -lock-timeout:

terraform apply -lock-timeout=5m

This waits up to 5 minutes for the lock to become available before failing. Useful when multiple pipelines might run close together.

Fix 7: Fix State File Corruption

If the state file is corrupted (maybe from a force-unlock during an active write), restore from backup:

S3 versioning:

# List state file versions
aws s3api list-object-versions --bucket my-terraform-state --prefix project/terraform.tfstate

# Restore a previous version
aws s3api get-object --bucket my-terraform-state --key project/terraform.tfstate --version-id VERSION_ID terraform.tfstate.backup

Pull the current state:

terraform state pull > current-state.json

Check if the state is valid JSON. If not, restore from the backup.

Push a fixed state:

terraform state push terraform.tfstate.backup

For the general state lock error article, see Fix: Terraform error acquiring state lock.

Fix 8: Use State Lock Alternatives

Disable locking (not recommended):

terraform apply -lock=false

This bypasses locking entirely. Only use this as a last resort for debugging. Running without locks in a team environment can corrupt the state file.

Use local state (for solo development):

terraform {
  backend "local" {
    path = "terraform.tfstate"
  }
}

Local backends still use file-system locks but do not have DynamoDB or network-related lock issues.

Still Not Working?

Check for network issues. If Terraform cannot reach DynamoDB or Terraform Cloud, it cannot acquire or release locks. Check your network connectivity and VPN status.

Check for AWS region mismatch. The DynamoDB table must be in the same region specified in the backend configuration. A mismatch causes lock acquisition failures.

Check for IAM session expiration. If your AWS session token expired during a long terraform apply, the lock release fails. Use longer session durations for Terraform operations.

Check for S3 bucket access. If Terraform cannot read/write the state file in S3, it fails before attempting the lock:

aws s3 ls s3://my-terraform-state/project/

Check for clock skew. DynamoDB conditional writes are timestamp-sensitive. If the CI runner’s clock is more than a few minutes off from AWS’s clock, lock entries can appear stale to one runner and fresh to another. Verify NTP is running on long-lived runners (timedatectl status) and that ephemeral runner images sync time at boot.

Check the backend block was reinitialized after a backend change. If someone modified dynamodb_table or key in the backend block and skipped terraform init -migrate-state, locks land in one place and state lookups happen in another. Run terraform init -reconfigure to force a clean re-read of the backend configuration, then retry.

Check for a wedged Terraform Cloud agent. Self-hosted Terraform Cloud agents that lose connection mid-run leave the workspace locked with no live agent attached. In the Terraform Cloud UI under Settings → Agents, check that the agent listed against the stuck run is still online. If not, restart the agent and unlock the workspace.

Consider using OpenTofu if you need open-source Terraform. OpenTofu is a fork that uses the same state format and locking mechanisms.

For Terraform provider installation failures, see Fix: Terraform failed to install provider. For drift between state and reality, see Fix: Terraform resource already exists.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles