Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Terraform By James Joyner IV · · 9 min read

Terraform Error Guide: 'Error acquiring the state lock' on plan/apply

Fix Terraform's 'Error acquiring the state lock': diagnose stale DynamoDB/blob locks, abandoned CI runs, expired credentials, and safely force-unlock state.

  • #terraform
  • #troubleshooting
  • #errors
  • #state

Overview

Terraform takes a lock on the state before it writes to it, so two concurrent runs cannot corrupt the same state file. When the backend already holds a lock — from a still-running apply, a crashed process, or a CI job that died mid-run — the new run cannot acquire it and aborts before doing any work.

You will see this on terraform plan, apply, or destroy:

Error: Error acquiring the state lock

Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
  ID:        9f1c4e2a-7b30-4a55-9d21-6f0e8b3c4d12
  Path:      my-tf-state/prod/network/terraform.tfstate
  Operation: OperationTypePlan
  Who:       runner@ci-build-4821
  Version:   1.7.5
  Created:   2026-06-23 13:41:02.118 +0000 UTC
  Info:

The lock is held in the backend, not locally: an S3 backend uses a DynamoDB item, an azurerm backend uses a blob lease, and gcs uses an object generation. The Who, Created, and Operation fields tell you who holds it and how stale it is.

Symptoms

  • plan/apply/destroy fail immediately with Error acquiring the state lock.
  • The Lock Info block names another user, CI runner, or a run from minutes/hours ago.
  • A previous run was Ctrl-C’d, lost network, or the CI pod was evicted.
  • Two pipelines target the same workspace at the same time.
terraform plan
Error: Error acquiring the state lock
Lock Info:
  ID:        9f1c4e2a-7b30-4a55-9d21-6f0e8b3c4d12
  Operation: OperationTypeApply
  Who:       jane@laptop
  Created:   2026-06-23 13:12:44 +0000 UTC

Common Root Causes

1. A genuinely concurrent run holds the lock

Another engineer or pipeline is mid-apply against the same state right now. This is the lock working as designed — not an error to force past.

terraform plan 2>&1 | grep -A6 "Lock Info"
Lock Info:
  Operation: OperationTypeApply
  Who:       deploy-bot@ci-build-4905
  Created:   2026-06-23 14:02:55 +0000 UTC

A Created time of seconds ago plus OperationTypeApply means wait, don’t unlock.

2. A crashed or cancelled run left a stale lock

A Ctrl-C, evicted CI pod, or lost VPN during apply leaves the lock behind because Terraform never reached the unlock step.

aws dynamodb get-item \
  --table-name terraform-locks \
  --key '{"LockID":{"S":"my-tf-state/prod/network/terraform.tfstate"}}'
{
    "Item": {
        "LockID": {"S": "my-tf-state/prod/network/terraform.tfstate"},
        "Info": {"S": "{\"ID\":\"9f1c4e2a-...\",\"Created\":\"2026-06-23T13:41:02Z\"}"}
    }
}

If Created is hours old and no process is running, the lock is stale.

3. Two pipelines share one state/workspace

Separate CI jobs (e.g. PR validation and a merge deploy) both run against the same backend key with no serialization, so they race for the lock.

grep -A4 'backend "s3"' backend.tf
backend "s3" {
  bucket         = "my-tf-state"
  key            = "prod/network/terraform.tfstate"
  dynamodb_table = "terraform-locks"
}

Two jobs with the same key will collide. Distinct workspaces or keys avoid it.

4. The lock table or lease backend is misconfigured

If dynamodb_table is missing on an S3 backend, locking is silently skipped — but on azurerm, a broken blob lease can leave a permanent lease that looks like a stuck lock.

az storage blob show \
  --container-name tfstate --name prod.terraform.tfstate \
  --account-name mytfstate --query "properties.lease" -o json
{
  "state": "leased",
  "status": "locked"
}

A leased/locked blob with no active run is a stale lease that must be broken.

5. Expired credentials abort apply mid-write

A short-lived STS/session token expires during a long apply, the process dies after locking but before unlocking, and the next run finds a stale lock.

aws sts get-caller-identity
An error occurred (ExpiredToken) when calling the GetCallerIdentity operation:
The security token included in the request is expired.

Refresh credentials first, otherwise even force-unlock may fail to reach the backend.

6. Clock skew or wrong backend key

Pointing at the wrong key/workspace shows a lock from an unrelated state, making it look held when your real state is fine.

terraform workspace show
terraform state list | head -1
default

Confirm you are operating on the state you think you are before unlocking anything.

Diagnostic Workflow

Step 1: Read the full lock info

terraform plan 2>&1 | sed -n '/Lock Info/,/^$/p'

Note Who, Operation, Created, and the lock ID — you need the ID to unlock.

Step 2: Decide if the run is live or dead

# CI: check whether the named runner/job is still active
gh run list --workflow deploy.yml --limit 5

If the Who job is still in_progress, wait. If it failed/cancelled, the lock is stale.

Step 3: Inspect the backend lock directly

# S3 + DynamoDB
aws dynamodb get-item --table-name terraform-locks \
  --key '{"LockID":{"S":"<STATE_PATH>"}}'
# azurerm
az storage blob show --container-name tfstate --name <KEY> \
  --account-name <ACCT> --query "properties.lease"

Confirm the lock’s age and that no process owns it.

Step 4: Refresh credentials

aws sts get-caller-identity || aws sso login

Make sure you can actually reach the backend before unlocking — a failed unlock can leave you worse off.

Step 5: Force-unlock with the exact lock ID, then retry

terraform force-unlock 9f1c4e2a-7b30-4a55-9d21-6f0e8b3c4d12
terraform plan

Only run force-unlock once you have confirmed no live run holds the lock.

Example Root Cause Analysis

A nightly deploy pipeline fails with Error acquiring the state lock on prod/network. The lock info reads:

Lock Info:
  Operation: OperationTypeApply
  Who:       runner@ci-build-4821
  Created:   2026-06-23 02:14:09 +0000 UTC

The named job ci-build-4821 is checked in CI and shows status cancelled — the runner was scaled down mid-apply at 02:14. No other run is active:

gh run list --workflow deploy.yml --limit 3
completed  failure   deploy  ci-build-4905
cancelled  cancelled deploy  ci-build-4821

This is a stale lock from the cancelled job, not a live conflict. Credentials are valid:

aws sts get-caller-identity
{"Account": "123456789012", "Arn": "arn:aws:sts::123456789012:assumed-role/deploy/..."}

Fix: force-unlock the stale ID and re-run.

terraform force-unlock 9f1c4e2a-7b30-4a55-9d21-6f0e8b3c4d12
terraform apply -auto-approve

The apply proceeds normally. The deeper fix is to serialize the pipeline so a new run cannot start while the previous one is unfinished.

Prevention Best Practices

  • Never force-unlock reflexively. Confirm the named Who/job is actually dead first — unlocking a live apply can corrupt state.
  • Serialize CI runs against each state with a concurrency group (e.g. one in-flight job per workspace) so pipelines never race for the lock.
  • Use long-lived enough credentials (or auto-refresh) so a long apply cannot expire mid-run and orphan a lock.
  • Set a backend timeout and prefer terraform apply over Ctrl-C-prone interactive runs in automation; trapping signals to unlock cleanly helps.
  • Keep one key/workspace per logical environment so unrelated runs never share a lock.
  • For triage, the free incident assistant can turn a lock-info block into a wait-or-unlock recommendation. More patterns in the Terraform guides.

Quick Command Reference

# See the full lock info
terraform plan 2>&1 | sed -n '/Lock Info/,/^$/p'

# Inspect the backend lock (S3 + DynamoDB)
aws dynamodb get-item --table-name terraform-locks \
  --key '{"LockID":{"S":"<STATE_PATH>"}}'

# Inspect the lease (azurerm)
az storage blob show --container-name tfstate --name <KEY> \
  --account-name <ACCT> --query "properties.lease"

# Confirm the named CI job is dead, not live
gh run list --workflow deploy.yml --limit 5

# Refresh credentials before unlocking
aws sts get-caller-identity || aws sso login

# Force-unlock with the exact ID (only when no live run holds it)
terraform force-unlock <LOCK_ID>

Conclusion

Error acquiring the state lock means the backend already holds a lock on your state. The usual root causes:

  1. A genuinely concurrent run holds it — wait, do not unlock.
  2. A crashed/cancelled run left a stale lock behind.
  3. Two pipelines share one key/workspace and race for the lock.
  4. A misconfigured lock table or stuck blob lease.
  5. Expired credentials killed an apply after it locked but before it unlocked.
  6. You are pointed at the wrong key/workspace and seeing an unrelated lock.

Always read the lock info, confirm the holder is truly dead, then force-unlock with the exact ID. The durable fix is serializing runs so the lock is never abandoned.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.