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
- Ensure every Lock has a matching Unlock (use defer), including on error paths.
- Call inmem.Reset() between tests to clear package-level state.
- 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
- Always pair Lock with a deferred Unlock, including on error paths.
- Call inmem.Reset() at the start of each test to clear package-level locks.
- Use unique workspace names per parallel test to avoid contention.
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
- state not locked
- invalid lock id
- failed to lock inmem state: %s
- consul lock was lost
- the state is already locked by another terraform client
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/6f18d53146626f0e.
Report an issue: GitHub.