hashicorp/terraform · error · LockError

HTTP remote state already locked: ID=

Error message

HTTP remote state already locked: ID=%s

What it means

The expected lock-conflict path: the lock endpoint returned 409 Conflict or 423 Locked, the body parsed into a statemgr.LockInfo, and the existing lock's ID is reported via '%s'. This is NOT a bug — it means another Terraform process (or a stale lock from a crashed run) currently holds the state lock.

Solutions

  1. Wait for the other run to finish and release the lock, then retry.
  2. If the ID is known to be stale (the other run is dead), run: terraform force-unlock <ID>.
  3. Prevent overlap with workspace-level locking/serialization in CI.
  4. If the server supports lock TTLs, enable them so stale locks expire automatically.

Example fix

# Given: HTTP remote state already locked: ID=abc-123
terraform force-unlock abc-123
Defensive patterns

Strategy: fallback

Validate before calling

# Pre-flight / operational: detect an existing lock before apply
# Many HTTP backends expose a GET on the lock URL; if you can, query it and surface the ID.
curl -sS -u "$TF_HTTP_USERNAME:$TF_HTTP_PASSWORD" "${TF_HTTP_LOCK_ADDRESS:-$TF_HTTP_ADDRESS}" || true

Try / catch

# On a lock conflict, surface the ID and offer force-unlock rather than looping blindly.
set +e
terraform apply -auto-approve
rc=$?
set -e
if [ $rc -ne 0 ] && grep -q 'HTTP remote state already locked: ID=' terraform.log; then
  id=$(sed -n 's/.*ID=\([0-9a-fA-F-]*\).*/\1/p' terraform.log | head -1)
  echo "Stale lock detected: $id — run: terraform force-unlock $id"
fi

Prevention

When it happens

Trigger: Concurrent terraform apply targeting the same state; a previous apply crashed or was killed leaving a stale lock; overlapping CI pipelines; a developer's interactive run still in progress.

Common situations: Two CI pipelines triggered on the same workspace; previous terraform apply was OOM-killed; long-running apply in another terminal; lock TTL not enabled so stale locks persist.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/0ddc4a8a9b77e96a. Report an issue: GitHub.

Appendix: 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 d32a084675)