Cloud Tech

Terraform Apply Stuck Acquiring State Lock

Problem this article addresses

A safe Terraform state lock troubleshooting guide for stuck apply runs, stale locks, lock IDs, force-unlock, backend behavior, and CI pipeline guardrails.

Published Jul 26, 2026Victor NwokeReviewed Jul 26, 20269 min read

Technical claims are reviewed against the cited primary sources. Hands-on guides include execution or diagnostic evidence when the article makes a tested-result claim.

Your Terraform apply is not frozen.

It may be waiting on a state lock that will never come back.

The terminal says:

text
Acquiring state lock. This may take a few moments...

No error. No progress bar. Just a blinking cursor and a quiet little decision: kill it, wait longer, or start poking the backend.

This happened to a team during a routine pipeline migration. A GitHub Actions job was cancelled mid-apply after a second push triggered a newer run. The runner stopped, but the lock it had acquired for the shared Terraform state did not cleanly disappear before the next run started. Every apply after that queued behind a lock nobody was actively using.

That is exactly the kind of incident state locking is designed to prevent and exactly why unlocking it casually is dangerous.

If you are still building the Terraform foundation, start with Terraform Basics. If the lock is happening inside automation, pair this with CI/CD Pipelines and Secure CI/CD Pipelines.

Why Terraform Locks State

Terraform state is the map between your configuration and the real infrastructure Terraform manages.

When Terraform runs an operation that could write state, it tries to lock the state first if the backend supports locking. HashiCorp's state locking documentation explains the reason directly: locking prevents other runs from acquiring the same state and potentially corrupting it.

That lock matters because two applies against the same state can both believe they are the one valid writer.

Without locking, this is the bad shape:

  1. Pipeline A reads state.
  2. Pipeline B reads the same state.
  3. Pipeline A changes infrastructure and writes state.
  4. Pipeline B changes overlapping infrastructure from an older view and writes state.
  5. The state file no longer tells a clean story.

State locking is the guardrail that makes Terraform say, in effect: one writer at a time.

What Acquiring State Lock Means

This message does not mean Terraform is hung:

text
Acquiring state lock. This may take a few moments...

Terminal showing terraform apply waiting at Acquiring state lock with a 60 second lock timeout

It means Terraform is waiting to acquire the backend lock before continuing.

The terraform apply command supports:

bash
terraform apply -lock-timeout=5m

The official terraform apply docs describe -lock-timeout=DURATION as the period Terraform retries while trying to acquire a lock, unless locking has been disabled. If the timeout expires and the lock is still held, Terraform should return an error instead of waiting forever.

I do not use this as the fix:

bash
terraform apply -lock=false

HashiCorp documents -lock=false, but also warns that disabling locking is dangerous when anyone else might run Terraform against the same workspace. In a team or CI environment, that flag is usually a way to turn a waiting problem into a state integrity problem.

Common Places Locks Live

The exact lock mechanism depends on the backend.

BackendLock behavior to know
HCP Terraform / Terraform EnterpriseState locking is handled by the platform workspace.
S3 backendCurrent Terraform S3 backend docs support native S3 lock files with use_lockfile = true; DynamoDB-based locking is documented as deprecated.
AzureRM backendThe azurerm backend stores state in Azure Blob Storage and supports state locking and consistency checking with Azure Blob Storage native capabilities.
GCS backendThe GCS backend supports state locking.
Local backendLocal state uses local system APIs for locking, but this is not a collaboration backend.

Important update for AWS teams: many older Terraform guides talk only about S3 plus DynamoDB for locking. Current HashiCorp S3 backend documentation says DynamoDB-based locking is deprecated and will be removed in a future minor version. Native S3 lock files are now the documented direction for S3 backend locking.

That does not mean your existing DynamoDB-backed state instantly stops working. It means new guidance should not pretend DynamoDB locking is the future-safe default.

First: Prove What Is Holding the Lock

Before unlocking anything, prove whether another Terraform run is actually active.

I check these places first:

  • CI system: GitHub Actions, Azure DevOps, GitLab CI, Jenkins, or whichever runner owns applies.
  • Pull requests: another environment or branch may be applying the same workspace.
  • Terraform Cloud or Enterprise workspace runs, if used.
  • Local terminals: someone may have a long-running apply, destroy, import, state, or console session.
  • Backend lock metadata: the lock ID, operation, user, path, version, and timestamp if your backend exposes it.

Terraform's state locking docs say force-unlock should only be used to unlock your own lock when automatic unlocking failed. That is the mental model to keep: you are not overriding Terraform because you are impatient; you are cleaning up a lock you have proven is stale.

What a Lock Error Tells You

When Terraform fails to acquire a lock, the error usually includes lock information.

It may include fields like:

text
Lock Info:
  ID:        11111111-2222-3333-4444-555555555555
  Path:      prod/network/terraform.tfstate
  Operation: OperationTypeApply
  Who:       runner@example
  Version:   1.14.0
  Created:   2026-07-26 02:17:40 +0000 UTC
  Info:

Terminal showing Terraform error acquiring the state lock with lock ID, path, operation, user, version, and created timestamp

The exact fields and formatting depend on Terraform version and backend, but the important part is the lock ID.

Terraform requires that unique lock ID for force-unlock. The official docs describe the lock ID as a nonce that helps ensure the unlock targets the correct lock.

Safe Force-Unlock Checklist

I do not start with force-unlock.

I use this checklist first:

  1. Confirm no CI job is currently applying the same workspace.
  2. Confirm no engineer is running Terraform locally against the same backend key or workspace.
  3. I check when the lock was created.
  4. I check who or what acquired the lock.
  5. I check whether the previous run was cancelled, killed, timed out, or crashed.
  6. Confirm the lock ID matches the lock you intend to remove.
  7. Announce the unlock in the team channel if this is a shared environment.
  8. I run a read-only terraform plan afterward to confirm Terraform and the real infrastructure still agree.

Only then:

bash
terraform force-unlock <LOCK_ID>

For non-interactive automation, Terraform also supports:

bash
terraform force-unlock -force <LOCK_ID>

That removes the confirmation prompt. It does not make the operation safer.

HashiCorp's force-unlock command reference is clear that the command does not modify infrastructure. That is true, but incomplete as an operational comfort. The risk is what happens after unlocking if another apply was still active: you can allow multiple writers against the same state.

CI Guardrails That Prevent Stuck Locks

The better fix is upstream in the pipeline.

I use a clear lock timeout:

bash
terraform apply -auto-approve -lock-timeout=10m

This gives a real Terraform run time to finish, but makes a stale lock visible as a failed job instead of a mystery hang.

In GitHub Actions, add workflow-level concurrency so two applies for the same environment do not run together:

yaml
concurrency:
  group: terraform-${{ github.ref }}-production
  cancel-in-progress: false

For apply jobs, cancel-in-progress: false is usually safer than auto-cancelling an active apply. A plan job can often be cancelled freely. An apply job is mutating infrastructure and writing state; treat it differently.

Also log enough context to investigate a lock later:

bash
terraform version
terraform workspace show
terraform init -input=false
terraform apply -input=false -auto-approve -lock-timeout=10m

If a lock error appears, preserve the job log. It may contain the lock ID you need to investigate safely.

Backend Examples

For newer S3 backend locking, HashiCorp documents use_lockfile = true:

hcl
terraform {
  backend "s3" {
    bucket       = "example-terraform-state"
    key          = "production/network/terraform.tfstate"
    region       = "us-east-1"
    use_lockfile = true
  }
}

In addition to access to the state object, the Terraform identity needs s3:GetObject, s3:PutObject, and s3:DeleteObject on the corresponding lock-file object. For this example, that object is production/network/terraform.tfstate.tflock.

For Azure Storage state, the azurerm backend uses Azure Blob Storage:

hcl
terraform {
  backend "azurerm" {
    use_azuread_auth     = true
    tenant_id            = "00000000-0000-0000-0000-000000000000"
    storage_account_name = "examplestate"
    container_name       = "tfstate"
    key                  = "production/network.terraform.tfstate"
  }
}

I do not hardcode backend credentials in configuration. HashiCorp's backend documentation warns that backend config values can be written into local .terraform files and plan files. I use environment variables or your CI identity model instead.

What Not to Do

I avoid these shortcuts:

  • I do not delete a backend lock manually before confirming the active run is dead.
  • I do not run terraform apply -lock=false in CI to "get past" a lock.
  • I do not run force-unlock from the wrong workspace or backend directory.
  • I do not ignore a lock timeout and immediately rerun the same failed pipeline.
  • I do not apply a saved plan if the backend configuration or credentials captured in that plan are stale.
  • I do not treat state as a normal file you can edit by hand.

Terraform gives you state commands for controlled state operations. It does not want you editing raw state JSON directly.

A Practical Incident Runbook

When terraform apply appears stuck on a lock:

bash
# 1. Wait long enough to avoid interrupting a healthy apply.
terraform apply -lock-timeout=10m

# 2. If it fails, save the lock info from the error.
# Look for ID, Path, Operation, Who, Version, Created.

# 3. Check CI for active apply jobs against the same environment.

# 4. Check with the team before unlocking shared production state.

# 5. If the lock is stale and you own the cleanup:
terraform force-unlock <LOCK_ID>

# 6. Re-check state against real infrastructure before applying again.
terraform plan

The most important step is not the command. It is proving that the lock is stale.

Final Takeaway

Terraform's state lock is not the enemy.

It is the thing preventing two writers from damaging the same source of truth.

When a pipeline cancellation leaves a stale lock behind, terraform force-unlock is the cleanup tool, not the first reflex. Confirm the run is dead, confirm the lock ID, unlock only the stale lock, then fix the CI behavior that created the incident.

Full walkthrough on building a safe Terraform CI pipeline, lock handling included, is up on CloudTechByVictor.com. Worth a read before your next migration, not after the pipeline hangs.

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement