hashicorp/terraform · error
Error locking state
Error message
Error locking state: %s
What it means
During a backend initialization/migration, after optional local-state deletion, Terraform tries to acquire the state lock via clistate.Locker.Lock(sMgr, "backend from plan"). If acquisition fails the raw error is wrapped. Competing processes, stale locks, or an unresponsive remote backend are typical root causes.
Solutions
- Confirm no other run is active (`ps`, CI queue) before forcing anything.
- If truly stale, force-release with `tofu force-unlock <lock-id>` (id is in the inner error) then retry.
- Verify the backend's locking prerequisite exists (e.g. DynamoDB table for s3 backend) and credentials/permissions are correct.
- Use `-lock-timeout=<duration>` to wait for a transient lock instead of failing immediately.
- For local state, ensure no editor or other tool holds the .terraform.tfstate.lock file open.
Example fix
// before tofu init # another process holds the lock # -> Error locking state: ... // after tofu force-unlock <lock-id-from-error> tofu init -lock-timeout=60s
Defensive patterns
Strategy: retry
Validate before calling
// Before init, probe that the state lock is acquirable / no stale lock exists.
func probeStateLockAcquirable(sMgr *clistate.LocalState, timeout time.Duration) error {
locker := clistate.NewLocker(timeout, views.NewStateLocker(views.StateLockerNoOp, nil))
if err := locker.Lock(sMgr, "probe"); err != nil {
return fmt.Errorf("state lock not acquirable: %w", err)
}
defer locker.Unlock()
return nil
} Try / catch
if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil {
if isLockHeldErr(err) {
diags = diags.Append(fmt.Errorf("Error locking state (held by another run; use 'tofu force-unlock <id>' if stale): %s", err))
} else {
diags = diags.Append(fmt.Errorf("Error locking state: %s", err))
}
return nil, diags
} Prevention
- Serialize CI jobs that touch the same state (mutex/queue).
- Always release locks: let commands finish; avoid SIGKILL mid-run.
- Confirm backend lock prerequisites (e.g. DynamoDB table) exist before init.
- Use `-lock-timeout=` to absorb brief contention instead of failing fast.
When it happens
Trigger: stateLocker.Lock returns an error on sMgr. Triggers: another `tofu`/`terraform` process already holds the lock, a previous crashed run left a stale lock in the backend/lock file, the backend's lock API is unreachable, or the local state file is locked at the OS level.
Common situations: Concurrent CI jobs on the same state; a killed process that did not release its lock; remote backend (e.g. S3+DynamoDB) where the lock table is missing or inaccessible; cross-region latency causing lock-acquire timeout.
Related errors
- Error asking for state migration action
- Error copying state from the previous %[1]q %[2]s to the…
- Error creating temporary directory
- Error loading state: %[3]s Terraform failed to load…
- Error loading state: %[3]s Terraform failed to load…
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/5f7147e6d3f8c938.
Report an issue: GitHub.
Appendix: source
Thrown at internal/command/meta_backend.go:1778
for _, localState := range localStates {
// We always delete the local state, unless that was our new state too.
if err := localState.WriteState(nil); err != nil {
diags = diags.Append(&errBackendMigrateLocalDelete{err})
return nil, diags
}
if err := localState.PersistState(nil); err != nil {
diags = diags.Append(&errBackendMigrateLocalDelete{err})
return nil, diags
}
}
}
}
if m.stateLock {
view := views.NewStateLocker(vt, m.View)
stateLocker := clistate.NewLocker(m.stateLockTimeout, view)
if err := stateLocker.Lock(sMgr, "backend from plan"); err != nil {
diags = diags.Append(fmt.Errorf("Error locking state: %s", err))
return nil, diags
}
defer stateLocker.Unlock()
}
// Store the metadata in our saved state location
s := sMgr.State()
if s == nil {
s = workdir.NewBackendStateFile()
}
s.Backend = &workdir.BackendConfigState{
Type: c.Type,
Hash: uint64(cHash),
}
err := s.Backend.SetConfig(configVal, b.ConfigSchema())
if err != nil {
diags = diags.Append(fmt.Errorf("Can't serialize backend configuration as JSON: %s", err))
return nil, diagsView on GitHub (pinned to d32a084675)