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

Terraform Error Guide: 'Error locking state: ConditionalCheckFailedException' (S3 backend DynamoDB lock table)

Fix Terraform 'Error locking state: ConditionalCheckFailedException' from the S3 backend DynamoDB lock table — stale locks, concurrent runs, and safe unlocks.

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

Exact Error Message

When Terraform uses an S3 backend with a DynamoDB lock table, a failure to acquire the lock surfaces as a ConditionalCheckFailedException coming directly from the DynamoDB PutItem call:

Error: Error acquiring the state lock

Error message: operation error DynamoDB: PutItem, https response
error StatusCode: 400, RequestID: 7QJ2K1..., api error
ConditionalCheckFailedException: The conditional request failed

Lock Info:
  ID:        9f8b3c21-7a44-4e9d-bf10-2c5d8e6a1f33
  Path:      my-bucket/env/prod/terraform.tfstate
  Operation: OperationTypePlan
  Who:       jjoyner@ci-runner-04
  Created:   2026-06-27 10:21:14.882104 +0000 UTC
  Info:
  Version:   1.8.5

Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and
try again. For most commands, you can disable locking with the
"-lock=false" flag, but this is not recommended.

The headline reads Error acquiring the state lock, but the giveaway for this specific case is the DynamoDB: PutItem ... ConditionalCheckFailedException line. That is the DynamoDB lock table refusing to create a new lock item because one already exists.

What the Error Means

The S3 backend stores your state file in S3 and uses a separate DynamoDB table to coordinate locking. Before any operation that could write state (plan with refresh, apply, destroy, state subcommands), Terraform calls PutItem on the lock table with a conditional expression that effectively says: “create this lock item only if an item with this LockID does not already exist.”

If an item with that LockID is already present, DynamoDB rejects the write with ConditionalCheckFailedException. Terraform interprets that rejection as “someone else holds the lock” and aborts.

So the error does not mean the table is broken or your credentials are wrong (those produce different errors). It means: a lock row already exists in the DynamoDB table for this state path. The question is why — and whether that lock is legitimately held by an active run or left behind as a stale artifact.

Common Causes

  • A concurrent run genuinely holds the lock. Another engineer or pipeline is running plan/apply against the same state right now. This is the lock working as designed.
  • A crashed or cancelled run left a stale lock item. If Terraform was killed (Ctrl-C twice, OOM kill, kill -9, a terminal disconnect) after writing the lock but before releasing it, the DynamoDB item survives. Every later run then hits ConditionalCheckFailedException.
  • A CI job was killed mid-apply. Pipeline timeouts, cancelled builds, or autoscaled runners reclaimed mid-run all terminate Terraform without unlocking. This is the most common source of stale locks in practice.
  • Wrong or missing dynamodb_table configuration. If two workspaces point at the same table and use the same key path, or if a recent backend change altered the key, lock IDs can collide unexpectedly.
  • IAM permission edge cases. Less common for this exact exception, but if the identity can PutItem to create a lock yet lacks DeleteItem, runs acquire locks they can never release — producing stale locks that look like ConditionalCheckFailedException on the next run.

How to Reproduce the Error

The cleanest way to reproduce a stale lock is to simulate a hard kill. In one terminal, start a long operation:

terraform plan

While that is running and holding the lock, kill the process abruptly (simulating a crashed CI runner):

# from another shell, force-kill the terraform process
pkill -9 terraform

Now run any locking command again:

terraform plan

Because the previous process never released the lock, the new PutItem fails its conditional check and you get the ConditionalCheckFailedException shown above. You can also reproduce the legitimate-contention version by simply running terraform plan in two terminals against the same backend at the same time.

Diagnostic Commands

Everything in this section is read-only. The goal is to confirm a lock exists, see who owns it, and decide whether it is active or stale — without modifying anything.

First, confirm your backend configuration so you know the exact table and key:

grep -R "dynamodb_table\|bucket\|key\|backend \"s3\"" -n *.tf
terraform {
  backend "s3" {
    bucket         = "my-bucket"
    key            = "env/prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

Confirm the lock table actually exists and is healthy:

aws dynamodb describe-table --table-name terraform-locks \
  --query 'Table.[TableName,TableStatus,KeySchema]'

The DynamoDB LockID for an S3 backend is the state path: <bucket>/<key>. Read the lock item directly to inspect the owner and timestamp:

aws dynamodb get-item \
  --table-name terraform-locks \
  --key '{"LockID": {"S": "my-bucket/env/prod/terraform.tfstate"}}' \
  --consistent-read
{
  "Item": {
    "LockID": { "S": "my-bucket/env/prod/terraform.tfstate" },
    "Info":   { "S": "{\"ID\":\"9f8b3c21-7a44-4e9d-bf10-2c5d8e6a1f33\",\"Operation\":\"OperationTypePlan\",\"Who\":\"jjoyner@ci-runner-04\",\"Created\":\"2026-06-27T10:21:14Z\"}" }
  }
}

The Created timestamp and Who field tell you everything: a lock from minutes ago by an active runner is probably legitimate; a lock from hours ago by a runner that no longer exists is almost certainly stale. If you are unsure which items are present, scan the table:

aws dynamodb scan --table-name terraform-locks \
  --projection-expression "LockID" --consistent-read

You can also reproduce the error to confirm the lock ID Terraform reports matches the item you found:

terraform plan

And verify the workspace state is otherwise intact:

terraform state list

Cross-check the Who/Created from the diagnostic output against your CI history or chat channels. Do not delete anything yet — confirm no active run owns this lock first.

Step-by-Step Resolution

  1. Rule out an active run. Check your CI/CD system, ask your team, and compare the Who and Created fields from the get-item output against running jobs. If anyone is mid-apply, wait. Force-unlocking an active apply can corrupt state — this is the one outcome you must avoid.

  2. Retry once. Transient contention often clears on its own. If a teammate’s plan finished in the last few seconds, simply re-running terraform plan succeeds with no further action.

  3. Use the lock ID Terraform reported. Terraform prints the exact ID in its error output (9f8b3c21-... above). You will pass that value to force-unlock — not the LockID/path.

  4. Force-unlock — the careful fix. Once you are certain the lock is stale and no run is active, release it through Terraform itself. This is the recommended remediation because it uses the backend’s own logic:

    terraform force-unlock 9f8b3c21-7a44-4e9d-bf10-2c5d8e6a1f33

    Terraform asks for confirmation; type yes. Caution: only do this after confirming no active operation holds the lock. Force-unlocking a live run can leave state half-written.

  5. Re-run your command. After a successful unlock, terraform plan/apply proceeds normally.

  6. Last resort: delete the DynamoDB item directly. If force-unlock itself fails (for example the lock ID is malformed, or the backend config no longer matches), you can remove the item manually. This bypasses all of Terraform’s safety checks — only do this when force-unlock cannot, and only after triple-confirming no run is active:

    # DESTRUCTIVE — last resort only, never during an active run
    aws dynamodb delete-item \
      --table-name terraform-locks \
      --key '{"LockID": {"S": "my-bucket/env/prod/terraform.tfstate"}}'

    Deleting the wrong item, or deleting a live lock, can corrupt state for everyone using that backend. Prefer force-unlock every time it works.

Prevention and Best Practices

  • Always exit Terraform cleanly. Avoid kill -9 and double Ctrl-C; let Terraform release the lock on its own.
  • Set generous CI timeouts and trap cancellations. Configure pipelines so a cancelled or timed-out apply still runs terraform force-unlock (or at least logs the lock ID) on teardown.
  • Grant complete IAM permissions on the lock table. The runner identity needs dynamodb:GetItem, PutItem, and DeleteItem on the table, plus s3:GetObject/PutObject on the state bucket. Missing DeleteItem is a classic source of permanent stale locks.
  • Serialize runs per state. Use pipeline concurrency groups so two jobs never target the same state simultaneously.
  • Keep one table per backend convention and never reuse key paths across unrelated workspaces.
  • For automated lock cleanup and on-call playbooks, our Incident Response toolkit can codify the “is this lock stale?” decision into a repeatable runbook.

Frequently Asked Questions

Is ConditionalCheckFailedException ever a permissions problem? Not directly. This specific exception means a lock item already exists. However, missing dynamodb:DeleteItem permission causes runs to acquire locks they can never release, which produces stale items that surface as this error on the next run. The root cause is IAM, but the symptom is a conditional check failure.

How do I find the lock ID to pass to force-unlock? Terraform prints it in the error output as the ID: field under Lock Info. You can also read the Info attribute of the DynamoDB lock item with aws dynamodb get-item. Pass that UUID to force-unlock, not the LockID/state path.

Is it safe to just delete the DynamoDB item? Only as a last resort and only when no run is active. terraform force-unlock is always preferred because it uses the backend’s own release logic. Manual delete-item skips every safety check and can corrupt shared state if you remove a live lock.

Why does this happen so often in CI? CI runners are frequently killed mid-job by timeouts, cancellations, or autoscaling. A killed process never releases its lock, leaving a stale item. Add cleanup-on-cancel logic and concurrency limits to your pipelines to prevent it.

Can I just run with -lock=false to get unblocked? You can, but you shouldn’t on shared state. Disabling locking removes the protection that prevents concurrent writes from corrupting your state file. Resolve the stale lock with force-unlock instead.

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.