hashicorp/terraform · error

lock ID '%s' does not match the existing lock ID '%s'

Error message

lock ID '%s' does not match the existing lock ID '%s'

What it means

Returned by unlockWithFile when the lock ID passed to Unlock(id) does not equal the ID field stored in the retrieved .tflock file. This is a deliberate safety check: Terraform only unlocks the lock it owns, preventing one client from forcibly releasing another client's lock.

Source

Thrown at internal/backend/remote-state/s3/client.go:543

		if cerr := getOutput.Body.Close(); cerr != nil {
			log.Warn(fmt.Sprintf("failed to close S3 object body: %v", cerr))
		}
	}()

	data, err := io.ReadAll(getOutput.Body)
	if err != nil {
		return fmt.Errorf("failed to read the body of the S3 object: %w", err)
	}

	lockInfo := &statemgr.LockInfo{}
	if err := json.Unmarshal(data, lockInfo); err != nil {
		return fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err)
	}
	lockErr.Info = lockInfo

	// Verify that the provided lock ID matches the lock ID of the retrieved lock file.
	if lockInfo.ID != id {
		return fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'", id, lockInfo.ID)
	}

	// Delete the lock file to release the lock.
	_, err = c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(c.bucketName),
		Key:    aws.String(c.lockFilePath),
	})

	if err != nil {
		return fmt.Errorf("failed to delete the lock file: %w", err)
	}

	log.Debug(fmt.Sprintf("Deleted lock file: '%q'", c.lockFilePath))

	return nil
}

func (c *RemoteClient) unlockWithDynamoDB(ctx context.Context, id string, lockErr *statemgr.LockError) error {

View on GitHub (pinned to c9def3e214)

Solutions

  1. Run 'terraform force-unlock' with the CURRENT lock ID shown in the latest 'Error acquiring the state lock' message, not an old one.
  2. Inspect the .tflock file contents to read the live lockInfo.ID.
  3. Coordinate with teammates/CI to ensure only one client owns the lock.
  4. If the stored lock is genuinely orphaned and you are sure, delete the .tflock file manually after confirming no active run.

Example fix

# before: using a stale lock id
terraform force-unlock a1b2c3-old
# after: fetch the current id from the latest lock message
terraform force-unlock <current-id-from-error>
Defensive patterns

Strategy: validation

Validate before calling

// Read the live lock ID and compare before calling Unlock
func currentLockID(ctx context.Context, s3c *s3.Client, bucket, lockKey string) (string, error) {
  out, err := s3c.GetObject(ctx, &s3.GetObjectInput{Bucket: &bucket, Key: &lockKey})
  if err != nil { return "", err }
  defer out.Body.Close()
  data, _ := io.ReadAll(out.Body)
  var li statemgr.LockInfo
  if err := json.Unmarshal(data, &li); err != nil { return "", err }
  return li.ID, nil
}

// usage
live, _ := currentLockID(ctx, s3c, bucket, lockKey)
if live != id { return fmt.Errorf("refusing unlock: live id %s != %s", live, id) }

Try / catch

if err := client.Unlock(id); err != nil {
    if strings.Contains(err.Error(), "does not match the existing lock ID") {
        // fetch the real id and re-issue force-unlock
    }
}

Prevention

When it happens

Trigger: Unlock(id) is called with an id that differs from lockInfo.ID parsed from the S3 lock file — e.g. operator passes the wrong force-unlock ID, or the lock was re-acquired by another process so the stored ID changed.

Common situations: Running 'terraform force-unlock <wrong-id>'; copying a lock ID from a stale log after another teammate re-locked the state; running two CI jobs against the same workspace where the second acquired a new lock.

Related errors


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