mastra-ai/mastra · warning

License server rate limited: ${data.reason}

Error message

License server rate limited: ${data.reason}

What it means

If the license validation response body carries code 'RATE_LIMITED' without an HTTP 429 status, performValidation throws 'License server rate limited: <reason>'. The source comment marks this as defensive: a throttle marker in the body is still a transient condition, not a verdict on the license, so it throws rather than marking the key invalid (which is what happens for INVALID_KEY/EXPIRED/REVOKED).

Source

Thrown at packages/core/src/license/index.ts:187

      }

      const data = (await response.json()) as LicenseValidationResponse;

      if (data.valid) {
        this.status = 'valid';
        this.logger?.info(`License validated${data.expiresAt ? `, expires ${data.expiresAt.slice(0, 10)}` : ''}`);
        this.cachedResult = data;

        const ttlSeconds = data.leaseTtlSeconds || this.DEFAULT_TTL_MS / 1000;
        this.cacheExpiry = now + ttlSeconds * 1000;
        this.gracePeriodEnd = now + this.GRACE_PERIOD_MS;

        this.scheduleRevalidation(ttlSeconds);
        return true;
      } else if (data.code === 'RATE_LIMITED') {
        // Defensive: a throttle marker in the body without a 429 status is
        // still transient, not a license verdict.
        throw new Error(`License server rate limited: ${data.reason}`);
      } else {
        this.status = 'invalid';
        this.logger?.error(`License validation failed: ${data.code} - ${data.reason}`);
        this.clearCache();
        return false;
      }
    } catch {
      // Network error or server unreachable
      if (this.cachedResult && now < this.gracePeriodEnd) {
        this.logger?.warn('License server unreachable. Using cached license (within grace period).');
        this.status = 'valid';
        this.scheduleRevalidation(this.DEFAULT_TTL_MS / 1000); // Retry later
        return true;
      } else if (this.cachedResult) {
        this.logger?.error('License server unreachable and grace period expired. Disabling enterprise features.');
        this.status = 'invalid';
        this.clearCache();
        return false;

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Back off and retry — RATE_LIMITED is transient; the key itself is fine
  2. Reduce validation frequency: cache the lease for the full leaseTtlSeconds and avoid tight revalidation loops
  3. Consolidate license checks: run one validator per host/pod and share the result internally
  4. Inspect proxies/gateways between client and license server that may mangle HTTP status codes

Example fix

// before
while (true) { await license.performValidation(); } // hammers server -> RATE_LIMITED
// after
try {
  await license.performValidation();
} catch (e) {
  if (String(e).includes('rate limited')) {
    await new Promise(r => setTimeout(r, 30_000)); // back off, then retry
    await license.performValidation();
  }
}
Defensive patterns

Strategy: retry

Try / catch

try {
  await licenseClient.validate();
} catch (e) {
  if (e.message.startsWith('License server rate limited')) {
    await new Promise(r => setTimeout(r, 30_000)); // exponential backoff
    await licenseClient.validate();
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: The license server responds 200 (or another non-429 status) but the JSON body contains valid:false, code:'RATE_LIMITED' — an inconsistent server response or intermediary stripping/rewriting status codes — while the client is exceeding validation request limits.

Common situations: A reverse proxy or API gateway collapsing error statuses into 200 responses; many processes/containers validating the same key concurrently and hitting server-side throttling; aggressive revalidation loops in a fleet of workers.

Related errors


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/dfc7fd179f029193. Report an issue: GitHub.