hashicorp/terraform · error

lock file exists

Error message

lock file %s exists

What it means

remoteClient.Lock first calls cosLock (a COS-side guard), then checks whether the lock info object already exists at c.lockFile via getObject. If a prior run left a lock info object there, exists==true and the lock is rejected. This means another run (live or stale) is holding the state lock.

Solutions

  1. Confirm no active run, then `terraform force-unlock <LOCK_ID>` (the ID is the md5 stored on the lock object).
  2. If force-unlock can't reach it, manually delete the lock object from COS at c.lockFile.
  3. Coordinate team access so only one run targets the workspace at a time.
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check whether a COS lock object is present before attempting Lock.
func lockFileExists(c *remoteClient) (bool, error) {
    exists, _, _, err := c.getObject(c.lockFile)
    if err != nil {
        return false, err
    }
    return exists, nil
}

// if exists, prompt for force-unlock instead of letting Lock fail.

Try / catch

// On lock failure, classify and offer force-unlock.
id, err := c.Lock(info)
if err != nil {
    var le *statemgr.LockError
    if errors.As(err, &le) {
        return fmt.Errorf("state already locked (id=%s); run terraform force-unlock %s", le.Info.ID, le.Info.ID)
    }
    return err
}

Prevention

When it happens

Trigger: Lock() -> cosLock succeeds -> getObject(c.lockFile) returns exists==true.

Common situations: Another Terraform run currently holds the lock; a previous run crashed and left a stale lock object; the lock object was written by an older Terraform version and never cleaned up.

Related errors


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

Appendix: source

Thrown at internal/backend/remote-state/cos/client.go:98

}

// Lock lock remote state file for writing
func (c *remoteClient) Lock(info *statemgr.LockInfo) (string, error) {
	log.Printf("[DEBUG] lock remote state file %s", c.lockFile)

	err := c.cosLock(c.bucket, c.lockFile)
	if err != nil {
		return "", c.lockError(err)
	}
	defer c.cosUnlock(c.bucket, c.lockFile)

	exists, _, _, err := c.getObject(c.lockFile)
	if err != nil {
		return "", c.lockError(err)
	}

	if exists {
		return "", c.lockError(fmt.Errorf("lock file %s exists", c.lockFile))
	}

	info.Path = c.lockFile
	data, err := json.Marshal(info)
	if err != nil {
		return "", c.lockError(err)
	}

	check := fmt.Sprintf("%x", md5.Sum(data))
	err = c.putObject(c.lockFile, data)
	if err != nil {
		return "", c.lockError(err)
	}

	return check, nil
}

// Unlock unlock remote state file

View on GitHub (pinned to d32a084675)