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

  1. Confirm no other run is active (`ps`, CI queue) before forcing anything.
  2. If truly stale, force-release with `tofu force-unlock <lock-id>` (id is in the inner error) then retry.
  3. Verify the backend's locking prerequisite exists (e.g. DynamoDB table for s3 backend) and credentials/permissions are correct.
  4. Use `-lock-timeout=<duration>` to wait for a transient lock instead of failing immediately.
  5. 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

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


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, diags

View on GitHub (pinned to d32a084675)