What Is a Terraform State Lock?
Imagine two engineers at Swiggy both notice the same infrastructure issue at 9am on Monday. Both run terraform apply at almost the same time. Both see a clean plan. Both hit enter. Without locking, both applies run simultaneously — both read the same state, both make changes, both write back a new state file. The second write overwrites the first. Resources get created twice. State ends up describing infrastructure that does not match reality. Nobody knows what actually exists.
A state lock prevents exactly this. The moment an apply starts, Terraform writes a lock record to DynamoDB. Any other terraform apply that tries to start reads that lock record and waits — or fails with a clear error explaining who holds the lock and when they acquired it.
+------------------------------------------+| Engineer A runs terraform apply || Lock acquired: LockID written to DynamoDB|+------------------------------------------+ | +-----------+-----------+ | | v v+------------------+ +-------------------+| Engineer A | | Engineer B || Apply proceeds | | terraform apply || normally | | Error: state is || | | locked by A since || | | 09:03:14 UTC |+------------------+ +-------------------+ | v+------------------------------------------+| Apply complete || Lock released: LockID deleted from table || Engineer B can now run apply |+------------------------------------------+How Locking Works with S3 and DynamoDB
When using the S3 backend with DynamoDB, Terraform performs these steps:
- Before apply starts: write a lock item to DynamoDB with key
LockID = <bucket>/<key> - If that item already exists: another process holds the lock — fail or wait
- During apply: the lock item stays in DynamoDB
- After apply completes (success or failure): delete the lock item
The lock item contains who holds it, when they acquired it, and what operation they are running — visible if you look in the DynamoDB table directly.
# What a lock looks like when another apply is runningterraform apply # Error: Error acquiring the state lock## Error message: ConditionalCheckFailedException: The conditional request failed# Lock Info:# ID: 8a3c2f91-4b5d-4e6f-9a2b-1c3d4e5f6a7b# Path: razorpay-terraform-state-ap-south-1/prod/payments-api/terraform.tfstate# Operation: OperationTypeApply# Who: priya@razorpay.com# Version: 1.6.3# Created: 2024-01-15 09:03:14.782341 +0000 UTC# Info:## 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.Breaking a Stuck Lock
Sometimes a lock is left behind — the engineer's laptop died mid-apply, the CI/CD runner was killed, or the network dropped. The lock item stays in DynamoDB but nobody is actually running an apply.
Before breaking a lock, verify that no apply is actually running:
# Check who holds the lock — look at the Created timestamp# If it is hours or days old and you confirmed no apply is running, break it terraform force-unlock <LOCK-ID># The LOCK-ID is the ID shown in the error message above# Example:terraform force-unlock 8a3c2f91-4b5d-4e6f-9a2b-1c3d4e5f6a7b # Terraform will ask for confirmation:# Do you really want to force-unlock?# Terraform will remove the lock on the remote state.# This will allow local Terraform commands to modify this state, even though it# may still be used by a running Terraform process.# There is no undo. Only 'yes' will be accepted to confirm.# Enter a value: yesCOMMON MISTAKE / WARNING**Security:** Never run `terraform force-unlock` while an apply might actually be in progress. Force-unlocking an active apply causes both applies to proceed simultaneously — exactly the race condition locking is designed to prevent. Confirm the original apply is dead before unlocking.
DynamoDB Table Requirements
The DynamoDB table for state locking must have exactly one attribute — LockID as the partition key of type String. Nothing else is required.
resource "aws_dynamodb_table" "terraform_locks" { name = "terraform-state-lock" billing_mode = "PAY_PER_REQUEST" # locks are infrequent — pay-per-request is cheapest hash_key = "LockID" # MUST be exactly "LockID" — Terraform expects this name attribute { name = "LockID" type = "S" } tags = { ManagedBy = "terraform" Purpose = "terraform-state-locking" }}Locking in Terraform Cloud
If you use Terraform Cloud as your backend, locking is built in — no DynamoDB table needed. Terraform Cloud handles locking automatically for every plan and apply, with a UI that shows who holds the lock and lets workspace admins force-unlock.
Troubleshooting State Locks
| Error | Root Cause | Fix |
|---|---|---|
Error acquiring the state lock |
Another apply is running | Wait for it to complete, or verify it is stuck and force-unlock |
ConditionalCheckFailedException |
DynamoDB locking conflict | Normal — another process holds the lock |
ResourceNotFoundException on lock table |
DynamoDB table does not exist | Create the table with LockID as partition key |
AccessDenied on DynamoDB |
Missing IAM permissions | Add dynamodb:GetItem, dynamodb:PutItem, dynamodb:DeleteItem |
| Lock stuck for hours | Apply crashed without releasing | Confirm no apply is running, then terraform force-unlock <ID> |
REMEMBER THIS**Remember:** The `-lock=false` flag disables locking entirely. Never use it in a shared team environment — it removes the only protection against simultaneous applies. It is only acceptable for read-only operations or in a single-engineer local setup.