hashicorp/terraform · error · LockInfo
Lock Info: ID: {{.ID}} Path: {{.Path}} Ope
Error message
Lock Info:
ID: {{.ID}}
Path: {{.Path}}
Operation: {{.Operation}}
Who: {{.Who}}
Version: {{.Version}}
Created: {{.Created}}
Info: {{.Info}} What it means
This is the multi-line template rendered by LockInfo.String() and returned (via LockInfo.Err()) when state-lock acquisition fails because another process holds the lock. The template emits ID, Path, Operation, Who (user@host), Version, Created, and Info of the competing lock holder so the user knows who/what is blocking them. The placeholders {{.ID}} etc. are filled from the LockError.Info captured by the failed Lock call.
Source
Thrown at internal/states/statemgr/locker.go:176
// don't error out on user and hostname, as we don't require them
userName := ""
if userInfo, err := user.Current(); err == nil {
userName = userInfo.Username
}
host, _ := os.Hostname()
info := &LockInfo{
ID: id,
Who: fmt.Sprintf("%s@%s", userName, host),
Version: version.Version,
Created: time.Now().UTC(),
}
return info
}
// Err returns the lock info formatted in an error
func (l *LockInfo) Err() error {
return errors.New(l.String())
}
// Marshal returns a string json representation of the LockInfo
func (l *LockInfo) Marshal() []byte {
js, err := json.Marshal(l)
if err != nil {
panic(err)
}
return js
}
// String return a multi-line string representation of LockInfo
func (l *LockInfo) String() string {
tmpl := `Lock Info:
ID: {{.ID}}
Path: {{.Path}}
Operation: {{.Operation}}
Who: {{.Who}}View on GitHub (pinned to d32a084675)
Solutions
- Wait for the other operation to finish, then retry — LockWithContext already backs off and retries until the context deadline.
- If the other holder is genuinely dead (crashed machine), use `terraform force-unlock <ID>` with the ID shown in the rendered lock info.
- Verify the Who/Path fields to confirm the holder is not your own stale process before force-unlocking.
- Do NOT force-unlock during an active cooperative run — it can corrupt state.
Example fix
// before
id, err := statemgr.LockWithContext(ctx, mgr, info)
if err != nil {
return err // shows raw template-ish error
}
// after
id, err := statemgr.LockWithContext(ctx, mgr, info)
if err != nil {
var le *statemgr.LockError
if errors.As(err, &le) && le.Info != nil {
return fmt.Errorf("state is locked by another run:\n%s\nif that run is gone, run: terraform force-unlock %s", le.Info.String(), le.Info.ID)
}
return err
} Defensive patterns
Strategy: try-catch
Validate before calling
n/a — lock conflict depends on remote state; pre-check by attempting Lock.
Type guard
func isLockError(err error) (*statemgr.LockError, bool) {
var le *statemgr.LockError
if errors.As(err, &le) {
return le, true
}
return nil, false
} Try / catch
id, err := statemgr.LockWithContext(ctx, mgr, info)
if le, ok := isLockError(err); ok && le.Info != nil {
return fmt.Errorf("state locked by %s@%s (id %s); run `terraform force-unlock %s` if stale", le.Info.Who, le.Info.Path, le.Info.ID, le.Info.ID)
} Prevention
- Use LockWithContext with a sensible timeout so transient locks clear automatically.
- Only force-unlock when you are certain the holder is dead.
- Serialize CI and local runs against the same workspace to avoid lock contention.
When it happens
Trigger: A Locker.Lock call returns a *LockError whose Info is the existing lock's metadata; LockWithContext or the CLI formats it via LockInfo.String()/Err() to present the conflict to the user.
Common situations: Two Terraform runs (apply/plan/destroy) targeting the same state concurrently; a previous run crashed without releasing the lock; a CI job and a developer running locally against the same backend; a long-running apply still in progress.
Related errors
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/a6cd89cac32d3f36.
Report an issue: GitHub.