hashicorp/terraform · error · LockError

state locked

Error message

state locked

What it means

Returned by lockMap.lock (internal/backend/remote-state/inmem/backend.go:186) wrapped in a statemgr.LockError when an entry already exists for the given state name. The inmem backend is a single shared global store (package-level 'locks'), so concurrent terraform processes/operations in the same binary collide on the same named state.

Source

Thrown at internal/backend/remote-state/inmem/backend.go:186

}

// Global level locks for inmem backends.
type lockMap struct {
	sync.Mutex
	m map[string]*statemgr.LockInfo
}

func (l *lockMap) lock(name string, info *statemgr.LockInfo) (string, error) {
	l.Lock()
	defer l.Unlock()

	lockInfo := l.m[name]
	if lockInfo != nil {
		lockErr := &statemgr.LockError{
			Info: lockInfo,
		}

		lockErr.Err = errors.New("state locked")
		// make a copy of the lock info to avoid any testing shenanigans
		*lockErr.Info = *lockInfo
		return "", lockErr
	}

	info.Created = time.Now().UTC()
	l.m[name] = info

	return info.ID, nil
}

func (l *lockMap) unlock(name, id string) error {
	l.Lock()
	defer l.Unlock()

	lockInfo := l.m[name]

	if lockInfo == nil {

View on GitHub (pinned to c9def3e214)

Solutions

  1. Ensure every Lock has a matching Unlock (use defer), including on error paths.
  2. Call inmem.Reset() between tests to clear package-level state.
  3. Use distinct workspace names per parallel test to avoid contention.

Example fix

// before: lock leaked on error
lockID, err := sm.Lock(info)
if err != nil { return err }
if doWork() != nil { return errors.New("failed") } // lock never released

// after: always unlock
lockID, err := sm.Lock(info)
if err != nil { return err }
defer sm.Unlock(lockID)
return doWork()
Defensive patterns

Strategy: try-catch

Try / catch

lockID, err := sm.Lock(info)
if err != nil {
    var le *statemgr.LockError
    if errors.As(err, &le) {
        // Already locked; surface the existing holder info to the user.
        return fmt.Errorf("state locked by %s since %s", le.Info.Who, le.Info.Created)
    }
    return err
}
defer sm.Unlock(lockID)

Prevention

When it happens

Trigger: Calling Lock on a RemoteClient whose Name is already present in the global locks map; running two concurrent operations against the same inmem workspace within the same process (common in tests).

Common situations: Test suites using the inmem backend that run operations in parallel without isolating workspaces; helper code that locks then errors before unlocking, leaving a stale lock; forgetting to call Unlock/force-unlock after a failed test operation.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/6f18d53146626f0e. Report an issue: GitHub.