1. A Stale Lock From A Crashed Or Cancelled Process
You run terraform apply, it hangs for 10 seconds, then you see Error: Error acquiring the state lock. What's happening is that another terraform process—maybe one you killed with Ctrl+C, or one that crashed during a plan—is still holding the lock in your backend. The backend (S3 + DynamoDB, or Consul, or TFE) sees an active lock record, so it blocks the new process.
The simplest fix: Run terraform plan first. If it says the lock is held by some random ID, you can force-unlock. But be careful—force-unlock can corrupt state if the original process is still running. Only do this if you're sure the process is dead.
terraform force-unlock <LOCK_ID>
The LOCK_ID is shown in the error message. On AWS S3 backend, run this:
aws dynamodb get-item --table-name terraform-lock --key '{"LockID": {"S": "your-state-key"}}' --region us-east-1
This shows you who holds the lock (Info column includes the PID and machine name). If you see a process that belongs to a CI/CD pipeline that already finished, or a session you closed, kill it. The standard way to kill is terraform force-unlock, but if that fails (it sometimes does with Consul backend), you can manually delete the lock item from DynamoDB or Consul KV. For DynamoDB:
aws dynamodb delete-item --table-name terraform-lock --key '{"LockID": {"S": "your-state-key"}}'
Why this works: The lock is just a row in DynamoDB with a TTL that defaults to never expire. Terraform refreshes the lock every few seconds. If the process dies, the refresh stops, but the row stays forever unless you delete it.
I've seen this happen most often in shared development environments where someone runs terraform in a tmux session, disconnects, and the session stays alive holding the lock for hours. Always check your running processes first. On Linux: ps aux | grep terraform.
2. Two CI/CD Pipelines Running At The Same Time
Team pipelines—GitLab, GitHub Actions, Jenkins—often lack proper locking between them. What happens is: Developer A pushes to branch main, a pipeline triggers terraform apply. Before that finishes, Developer B merges another PR, and another pipeline starts. Both try to grab the same state lock. The second pipeline gets the contention error.
The real fix is prevention: Don't force-unlock in a pipeline. That leads to state corruption. Instead, add a concurrency gate in your CI/CD workflow. For GitHub Actions, set concurrency: terraform_deploy in your workflow YAML:
concurrency:
group: terraform-${{ github.ref }}
cancel-in-progress: false
For GitLab, use resource_group in your job:
resource_group: terraform-apply
Why the concurrency fix works: It queues the second pipeline job instead of running it immediately. The first job finishes, releases the lock, then the second job runs cleanly. No lock contention, no manual unlock needed.
If you already have a stuck pipeline job that's holding the lock, you need to cancel that job in CI (not with terraform) and then force-unlock. I've seen teams waste hours force-unlocking manually only to have it happen again because they didn't add the concurrency group. Do the concurrency fix first.
3. Misconfigured DynamoDB Table For State Locking
Sometimes the lock error isn't about a stuck lock—it's about the backend itself being broken. Common issue: your terraform backend config references a DynamoDB table that doesn't exist or has wrong permissions. The error message might say Error acquiring the state lock but the real cause is a failed condition check in DynamoDB.
The fix: Verify the DynamoDB table exists and has the correct schema. Terraform expects a table with a primary key named LockID of type String. If someone renamed the key or added a sort key, locking fails.
aws dynamodb describe-table --table-name terraform-lock
Check that KeySchema has exactly one key: AttributeName: LockID, KeyType: HASH. Also check the IAM role running terraform has dynamodb:PutItem, dynamodb:GetItem, dynamodb:DeleteItem, and dynamodb:ConditionCheckItem on that table. If you're running terraform locally with AWS CLI, your profile needs those permissions. If you're in a pipeline, the role assigned to the runner needs them.
Why this works: Terraform's DynamoDB locking uses a conditional write (ConditionCheckItem) to atomically check if a lock exists and then write a new one. If the table doesn't have the right key or permissions, the conditional write fails, terraform interprets it as a lock conflict, and shows the generic contention error. It's a misleading error, but once you know this pattern, you can fix it in 2 minutes.
I've personally hit this when migrating from one AWS account to another and forgot to update the DynamoDB table name in the backend config. The old table was in account A, but terraform was now running from account B with a role that had no access to account A's table. The error looked like a lock contention, but it was a permission issue.
Quick Reference: When To Use Each Fix
| Scenario | Error message hint | Fix |
|---|---|---|
| Stale lock from crashed process | LockInfo: { "ID": "xxxx" } with old timestamp |
terraform force-unlock or delete DynamoDB row |
| Two CI/CD pipelines overlapping | Error appears in pipeline logs but not locally | Add concurrency group in CI config |
| Misconfigured DynamoDB table | Error happens every time, even on fresh state | Fix table schema, permissions, or backend config |
One last tip: if you're using S3 backend with DynamoDB, add a short lock timeout. In your backend config, set max_lock_retries = 10 and retry_base_interval = 2s. This gives your pipeline a 20-second window to wait for a lock to release before erroring out. But it's a band-aid—the real fix is addressing the root cause from the table above.