hashicorp/terraform · error
Error locking state: %s
Error message
Error locking state: %s
What it means
In Meta.backend_C_r_s (meta_backend.go:1778) Terraform tried to acquire the state lock (clistate.Locker.Lock) before writing the freshly reconfigured single-state backend into the workdir-state file and failed. The wrapped `%s` is the underlying lock error, typically 'state is locked' with the ID and info of whoever holds it, or a transport error reaching the remote backend.
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 c9def3e214)
Solutions
- Inspect the wrapped error for the lock holder info; wait for that process to finish or coordinate to stop it.
- If the lock is genuinely stale (the holder is gone), run `terraform force-unlock <LOCK_ID>` then retry init.
- For network/auth errors, verify credentials, endpoint, and connectivity to the backend, then retry.
- Use `-lock-timeout=<duration>` to make init wait for the lock instead of failing immediately.
Example fix
// before: terraform init (fails: Error locking state: state is locked) // after: terraform force-unlock <LOCK_ID> && terraform init
Defensive patterns
Strategy: retry
Validate before calling
// Before init, confirm no stale lock exists on the backend.
// (Authoritative check is backend-specific; shown as a preflight concept.)
func ensureStateUnlocked(runner func() error, lockTimeout time.Duration) error {
return retry(lockTimeout, time.Second*2, runner) // wait out a transient holder
} Try / catch
// On 'Error locking state', distinguish stale vs active and act accordingly.
if err := cmd.Init(); err != nil && strings.Contains(err.Error(), "Error locking state") {
if id := extractLockID(err.Error()); id != "" && lockLooksStale(err) {
_ = cmd.ForceUnlock(id) // only after confirming the holder is gone
return cmd.Init()
}
} Prevention
- Use `-lock-timeout=<duration>` in CI so init waits rather than failing on a short-lived lock.
- Never run concurrent Terraform commands against the same state.
- Ensure crashed runs are cleaned up (force-unlock) before the next run.
- Validate backend credentials and connectivity before running init.
When it happens
Trigger: Another Terraform process (or CI job) currently holds the state lock on the same backend; a previous run crashed without releasing the lock leaving a stale lock object; network/credentials failure contacting the locking backend (S3 DynamoDB, Consul, Azure, GCS, etc.).
Common situations: Concurrent CI pipelines targeting the same state, a developer's `terraform apply` still running in another shell, a killed run leaving a dangling lock, expired/temporary cloud credentials while locking.
Related errors
- Failed to lock cos state: %s
- error uploading state: %v
- %s (lock ID: "%s/%s")
- lock ID does not match existing lock
- lock ID %q does not match existing lock ID "%s/%s"
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/5f7147e6d3f8c938.
Report an issue: GitHub.