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 endpoint | Matching path | Limit | Window |
|---|---|---|---|
widget-search | /api/widget/search* | 200 | 1 min |
widget-suggestions | /api/widget/suggestions* | 300 | 1 min |
widget-compare | /api/widget/.../compare* | 10 | 1 min |
widget-analytics | /api/widget/.../analytics* | 500 | 1 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 endpoint | Matching path | Limit | Window |
|---|---|---|---|
sync | /api/plugin/shops/{shopId}/products/sync | 100 | 1 min |
shop-info | /api/plugin/shops/me | 60 | 1 min |
verification | /api/plugin/shops/{shopId}/verification-data | 20 | 1 min |
verify-action (POST) | /api/plugin/shops/{shopId}/verify | 5 | 5 min |
unverify-action (DELETE) | /api/plugin/shops/{shopId}/verify | 5 | 5 min |
Response headers
| Header | When emitted | Meaning |
|---|---|---|
X-RateLimit-Limit | Every /api/widget/* response | Total quota for this endpoint |
X-RateLimit-Remaining | Every /api/widget/* response | Remaining requests in the current window |
Retry-After | Only on 429 | Seconds 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:
| Concept | What it controls | Where you see it | Granularity |
|---|---|---|---|
| Rate limit (this page) | Per-minute / per-five-minute peaks | X-RateLimit-* headers, 429 error | Per endpoint, identical for everyone |
| Plan quota | Total monthly volume and catalog size | GET /api/plugin/shops/me → maxProducts, productsCount, maxSearchesPerMonth, searchesThisMonth | Per 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/mefor ~5 min on the client like the official WordPress plugin does; even though the plugin filter is not active, the endpoint is relatively expensive andproductsCount/searchesThisMonthchange slowly. - Don't abuse
widget-compare(10/min): cache results or batch comparisons. - Sync products in batches up to 500 (the plugin's
BATCH_SIZEcap) and space the calls; thesyncbucket will allow 100 req/min once the filter is activated. - Don't retry on
400,401,403,404or422— those are deterministic, retrying does not help. - Honor
Retry-Afterstrictly. 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.