Short answer: Terraform keeps a lock while it changes state. If another run is active, wait. If a run crashed or a CI job was cancelled, the lock stays behind, and you can remove it safely with terraform force-unlock after confirming nothing is running.
The error
terminal
Error: Error acquiring the state lock
Lock Info:
ID: 3f2a9c1e-...
Path: my-tf-state/prod/terraform.tfstate
Operation: OperationTypeApply
Who: runner@ci-12345
Created: 2026-10-09 14:02:11 UTC
Why it happens
| Cause | How to tell |
|---|---|
| Another plan or apply is running | The Who field shows an active CI job or a colleague |
| A CI job was cancelled or timed out | The job in Who is finished, but the lock remains |
| A local run was interrupted | Who shows your machine and an old Created time |
| Two pipelines target the same state | Locks appear whenever both run at the same time |
Fix it safely
- Read Who and Created in the error. Confirm that process is really gone (CI job finished, no terminal still running).
- Remove the lock using the ID from the error:
unlock.sh
terraform force-unlock 3f2a9c1e-...
Never force-unlock while an apply is still running. Two applies on the same state can corrupt it. If in doubt, wait for the CI job timeout or cancel it first.
Prevent it
Serialize runs per state in CI and let Terraform wait briefly for a lock instead of failing immediately.
.github/workflows/terraform.yml
# GitHub Actions: one run at a time per environment
concurrency:
group: terraform-prod
cancel-in-progress: false
# and in the job
- run: terraform apply -lock-timeout=5m -auto-approve
With the S3 backend, Terraform 1.10 and later can lock using S3 itself (use_lockfile = true), without a DynamoDB table. Check the backend documentation for your version before migrating.
backend.tf
terraform {
backend "s3" {
bucket = "my-tf-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
}
}