hashicorp/terraform · error · statemgr.LockError
HTTP remote state already locked: ID=%s
Error message
HTTP remote state already locked: ID=%s
What it means
The legitimate 'state is already locked' outcome: the server returned 409/423, the body parsed, and the holder's lock ID is reported. This is not a bug — it means another terraform process (or a crashed one that left a stale lock) currently holds the state lock. Returned as a statemgr.LockError carrying the parsed LockInfo of the holder.
Source
Thrown at internal/backend/remote-state/http/client.go:119
return "", fmt.Errorf("HTTP remote state endpoint invalid auth")
case http.StatusConflict, http.StatusLocked:
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", &statemgr.LockError{
Err: fmt.Errorf("HTTP remote state already locked, failed to read body"),
}
}
existing := statemgr.LockInfo{}
err = json.Unmarshal(body, &existing)
if err != nil {
return "", &statemgr.LockError{
Err: fmt.Errorf("HTTP remote state already locked, failed to unmarshal body"),
}
}
return "", &statemgr.LockError{
Info: &existing,
Err: fmt.Errorf("HTTP remote state already locked: ID=%s", existing.ID),
}
default:
return "", fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
}
func (c *httpClient) Unlock(id string) error {
if c.UnlockURL == nil {
return nil
}
resp, err := c.httpRequest(c.UnlockMethod, c.UnlockURL, &c.jsonLockInfo, "unlock")
if err != nil {
return err
}
defer resp.Body.Close()
switch resp.StatusCode {View on GitHub (pinned to c9def3e214)
Solutions
- Confirm the holder (ID, Who, Operation from the error) is not an active run you should wait for.
- If the holder is a dead/crashed run, run `terraform force-unlock <ID>` using the ID printed in the message.
- Add pre-run coordination (CI queueing, branch protection) to avoid concurrent applies on shared state.
Example fix
# once you confirm the holder is stale: terraform force-unlock <ID-printed-in-the-error>
Defensive patterns
Strategy: try-catch
Type guard
func isAlreadyLocked(err error) (id string, ok bool) {
var le *statemgr.LockError
if !errors.As(err, &le) || le.Err == nil { return "", false }
s := le.Err.Error()
if !strings.Contains(s, "already locked: ID=") { return "", false }
return strings.TrimPrefix(s, "...ID="), true // adjust prefix to actual message
} Try / catch
err = sm.Lock(info)
if id, ok := isAlreadyLocked(err); ok {
if holderIsActive(id) { waitThenRetry() } else { promptForceUnlock(id) }
return
} Prevention
- Serialize applies on shared state (CI queue, branch protection).
- Confirm the holder is stale before force-unlock.
- Alert on frequent lock conflicts as a process smell.
When it happens
Trigger: Two engineers (or CI + engineer) running terraform apply against the same state simultaneously; a previous run crashed without unlocking; a long-running apply holding the lock. Fires at lock acquisition during plan/apply/destroy.
Common situations: Shared state with no lock coordination; killed CI run leaving a stale lock; interactive apply still running when a scheduled pipeline starts.
Related errors
- HTTP remote state endpoint requires auth
- HTTP remote state endpoint invalid auth
- HTTP remote state already locked, failed to read body
- HTTP remote state already locked, failed to unmarshal body
- Unexpected HTTP response code %d
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/0ddc4a8a9b77e96a.
Report an issue: GitHub.