Terraform State Locking Preventing Conflicts in Teams
Remote state solves the problem of sharing a state file across a team. But sharing introduces a new problem: what happens when two engineers run terraform apply at the same time? Without state locking, both operations read the same state, calculate their changes, and write back a result — one overwriting the other. State locking prevents this by ensuring only one Terraform operation can write to the state at any given time.
What State Locking Does
When Terraform begins an operation that modifies state (apply, destroy, or state manipulation commands), it writes a lock to the backend. Any other Terraform process that tries to start a state-modifying operation while the lock exists will fail immediately with an error and wait — preventing two operations from running at the same time.
Diagram: How Locking Works
Engineer A starts "terraform apply"
|
v
Terraform writes lock to DynamoDB:
LockID = "my-project/terraform.tfstate"
Owner = "engineer-a@company.com"
Time = "2024-01-15T10:00:00Z"
|
v
Engineer B starts "terraform apply" at the same time
|
v
Terraform checks DynamoDB:
Lock already exists! Owner: engineer-a
|
v
Error: Error acquiring the state lock
Lock Info:
ID: abc-123
Owner: engineer-a@company.com
Created: 2024-01-15T10:00:00Z
|
v
Engineer B must wait until Engineer A's apply finishes
and the lock is released automatically.
Backends That Support Locking
Not every backend supports locking. The most commonly used backends and their locking support:
| Backend | Locking Mechanism |
|---|---|
| S3 + DynamoDB | DynamoDB table stores the lock (must create it manually) |
| Azure Blob Storage | Native blob lease (built in, no extra setup) |
| Google Cloud Storage | Native object locking (built in) |
| Terraform Cloud | Built-in automatic locking |
| Local | File-based lock (only works on one machine) |
DynamoDB Lock Table Setup for S3 Backend
When using the S3 backend, you must create a DynamoDB table before Terraform can use locking. The table must have a primary key named LockID of type String.
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = {
Purpose = "Terraform state locking"
}
}
Then reference it in the backend configuration:
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-locks" # <-- enables locking
encrypt = true
}
}
Handling Stuck Locks
Locks release automatically when a Terraform operation finishes — whether it succeeds or fails. However, if a Terraform process is interrupted (network cut, process killed, machine crash), the lock may remain even though no operation is running. This is called a stuck lock.
Terraform shows you the lock details when it cannot acquire a lock. If you confirm no operation is actually running (check with your team), you can force-release the lock:
terraform force-unlock LOCK_ID
Replace LOCK_ID with the ID shown in the error message. This command requires confirmation and should only be used when you are certain the lock is stale — not when another operation is genuinely running.
Disabling Locking Temporarily
In rare situations (troubleshooting, emergency recovery), you can run a Terraform operation without acquiring a lock. This is dangerous and should almost never be used in normal workflows.
terraform apply -lock=false
Never use this in a shared environment unless you are absolutely certain no one else is running Terraform and you accept the risk of state corruption.
Locking and CI/CD Pipelines
CI/CD pipelines frequently run Terraform applies. Without locking, two pipeline runs triggered at the same time race against each other. With locking, the second pipeline's apply waits until the first completes — or fails with a clear error if the wait timeout is exceeded.
In GitHub Actions or similar tools, serialise your Terraform jobs using concurrency groups to avoid lock contention entirely:
# In GitHub Actions workflow
concurrency:
group: terraform-${{ github.ref }}
cancel-in-progress: false
This tells GitHub Actions to allow only one job per branch at a time, queuing the second rather than cancelling it. State locking then acts as a final safety net.
Key Points
- State locking prevents two Terraform operations from modifying state simultaneously and causing corruption.
- For the S3 backend, a DynamoDB table with a
LockIDhash key provides the locking mechanism. - Azure Blob and GCS backends have native locking — no additional resources are needed.
- Use
terraform force-unlock LOCK_IDonly when a lock is confirmed stale — never while an operation is running. - In CI/CD pipelines, combine concurrency controls with state locking for the safest automated apply experience.
