Skip to main content

Rate limits

Neuroon enforces rate limiting per endpoint, not per subscription plan. Plan quotas (synced products, monthly searches) are a separate concept from request throttling. See Plan quotas vs rate limit below.

Current throttling state

Today the only filter actually firing is WidgetRateLimitFilter (/api/widget/*, authenticated with X-Widget-Token). The RateLimitFilter for the Plugin API exists in code but its matcher (path.contains("/from-plugin") in RateLimitFilter.java) does not match any path served by the current controllers — real plugin paths are /api/plugin/shops/me, /api/plugin/shops/{shopId}/products/sync, etc., without the /from-plugin suffix. While this stays the case, the Plugin API applies no throttling, even though the buckets are configured.

Production limits — Widget API

Internal endpointMatching pathLimitWindow
widget-search/api/widget/search*2001 min
widget-suggestions/api/widget/suggestions*3001 min
widget-compare/api/widget/.../compare*101 min
widget-analytics/api/widget/.../analytics*5001 min

Values are in RateLimitService.java. Granularity: per shop (shopId extracted from the X-Widget-Token).

Configured limits — Plugin API (not enforced yet)

These values are in code but the filter does not fire today. They are documented here because they will likely be activated in an upcoming release; plan with them in mind:

Internal endpointMatching pathLimitWindow
sync/api/plugin/shops/{shopId}/products/sync1001 min
shop-info/api/plugin/shops/me601 min
verification/api/plugin/shops/{shopId}/verification-data201 min
verify-action (POST)/api/plugin/shops/{shopId}/verify55 min
unverify-action (DELETE)/api/plugin/shops/{shopId}/verify55 min

Response headers

HeaderWhen emittedMeaning
X-RateLimit-LimitEvery /api/widget/* responseTotal quota for this endpoint
X-RateLimit-RemainingEvery /api/widget/* responseRemaining requests in the current window
Retry-AfterOnly on 429Seconds to wait before retrying (equals window size)

Successful response:

HTTP/1.1 200 OK
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 187
Content-Type: application/json

429 response body

When the bucket is exhausted, the response is:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 200
X-RateLimit-Remaining: 0
Retry-After: 60
Content-Type: application/json

{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please try again later.",
"retryAfter": 60,
"limit": 200,
"remaining": 0,
"timestamp": "2026-05-06T10:15:00Z"
}

Exponential backoff

Retry only on 429 and 5xx. Honor Retry-After. Recommended pattern: 1s, 2s, 4s, 8s with jitter, max 3–5 attempts.

import pRetry, { AbortError } from 'p-retry';

async function search(query: string) {
return pRetry(async () => {
  const res = await fetch('https://api.neuroon.ai/api/widget/search', {
    method: 'POST',
    headers: {
      'X-Widget-Token': process.env.NEUROON_WIDGET_TOKEN!,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query }),
  });
  if (res.status === 429 || res.status >= 500) {
    const retryAfter = Number(res.headers.get('Retry-After')) || 0;
    if (retryAfter > 0) await new Promise(r => setTimeout(r, retryAfter * 1000));
    throw new Error(`Retryable ${res.status}`);
  }
  if (!res.ok) throw new AbortError(`Non-retryable ${res.status}`);
  return res.json();
}, { retries: 3, factor: 2, minTimeout: 1000, randomize: true });
}

Plan quotas vs rate limit

These are two distinct layers. Don't conflate them:

ConceptWhat it controlsWhere you see itGranularity
Rate limit (this page)Per-minute / per-five-minute peaksX-RateLimit-* headers, 429 errorPer endpoint, identical for everyone
Plan quotaTotal monthly volume and catalog sizeGET /api/plugin/shops/memaxProducts, productsCount, maxSearchesPerMonth, searchesThisMonthPer shop, depends on plan

The plan tier (BASIC, GROWTH, PRO, ENTERPRISE) affects maxProducts and maxSearchesPerMonth returned by /shops/me, not the per-endpoint rate limits, which are global. If your integration needs more monthly volume, contact support to upgrade; per-endpoint rate limits do not scale by plan.

Best practices

  • Cache /api/plugin/shops/me for ~5 min on the client like the official WordPress plugin does; even though the plugin filter is not active, the endpoint is relatively expensive and productsCount / searchesThisMonth change slowly.
  • Don't abuse widget-compare (10/min): cache results or batch comparisons.
  • Sync products in batches up to 500 (the plugin's BATCH_SIZE cap) and space the calls; the sync bucket will allow 100 req/min once the filter is activated.
  • Don't retry on 400, 401, 403, 404 or 422 — those are deterministic, retrying does not help.
  • Honor Retry-After strictly. Ignoring it can lead to manual blocking.

Further reading

  • Errors — full structure of the rest of 4xx/5xx errors.
  • Shop API Key — generation, rotation and Origin validation.
  • Widget Token — 24h TTL and rotation.