hashicorp/terraform · error

invalid lock id: %q. current id: %q

Error message

invalid lock id: %q. current id: %q

What it means

Returned by LocalState.Unlock (local_state.go:217) when the supplied lock id does not match s.lockID. Lock ids are opaque tokens returned by Lock; Unlock requires the same token to prevent one consumer from releasing a lock acquired by another. When the mismatch is detected, Unlock also attempts to read the on-disk lock info so the returned LockError carries diagnostic context about who actually holds the lock.

Source

Thrown at internal/command/clistate/local_state.go:218

		}

		return "", lockErr
	}

	s.lockID = info.ID
	return s.lockID, s.writeLockInfo(info)
}

func (s *LocalState) Unlock(id string) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	if s.lockID == "" {
		return fmt.Errorf("LocalState not locked")
	}

	if id != s.lockID {
		idErr := fmt.Errorf("invalid lock id: %q. current id: %q", id, s.lockID)
		info, err := s.lockInfo()
		if err != nil {
			idErr = errors.Join(idErr, err)
		}

		return &statemgr.LockError{
			Err:  idErr,
			Info: info,
		}
	}

	os.Remove(s.lockInfoPath())

	fileName := s.stateFileOut.Name()

	unlockErr := s.unlock()

	s.stateFileOut.Close()

View on GitHub (pinned to c9def3e214)

Solutions

  1. Always use the exact id returned by the corresponding Lock call — store and forward it verbatim: `id, _ := s.Lock(info); ...; s.Unlock(id)`.
  2. Never fabricate or hard-code a lock id; if you lost the id, read it via lockInfo() or force-clear the lock manually rather than guessing.
  3. When forwarding ids across processes/commands, persist the id in a single source of truth (e.g. a file) and re-read it before Unlock.
  4. If the mismatch is intentional (force unlock), use the backend's force-unlock subcommand with the id printed by the holder, not an arbitrary value.

Example fix

// before
id, _ := s1.Lock(info)
s2.Unlock(id) // different instance, or stale id -> invalid lock id

// after: one owner, one id
id, err := s.Lock(info)
if err != nil { return err }
defer s.Unlock(id)
Defensive patterns

Strategy: validation

Validate before calling

// Validate that the id matches the held lock before unlocking
if id != s.lockID {
    return fmt.Errorf("refusing unlock: supplied id %q does not match held id %q", id, s.lockID)
}
return s.Unlock(id)

Type guard

// OwnsLock reports whether the given id is the one currently held by this LocalState.
func (s *LocalState) OwnsLock(id string) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    return id != "" && id == s.lockID
}

Prevention

When it happens

Trigger: Passing a hard-coded or stale lock id to Unlock; passing an id from a different LocalState instance; passing an empty string when a real id was returned; a wrapper that stores the wrong id; calling Unlock with the id of a lock acquired by a concurrent process (which is not the same instance).

Common situations: A backend wrapper caches the wrong id after a retry; test code reuses a constant id; a CLI command forwards a lock id from plan to apply but the apply uses a fresh LocalState whose lock has a different id; user-supplied `-lock-id` flag mismatches the actual lock.

Related errors


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