Skip to content

Commit 0dfb793

Browse files
tofikwestclaude
andcommitted
fix(integrations-catalog): add global request pacing to prevent 429s
Adds SYNC_MIN_INTERVAL_MS (default 100ms) to enforce a minimum gap between HTTP requests across all workers. Paces the sync to stay under the CompAI internal API throttle, which was triggering 429s at higher concurrency. Also logs backoff delays when retries fire for visibility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3bb0337 commit 0dfb793

2 files changed

Lines changed: 12 additions & 0 deletions

File tree

tools/integrations-catalog-sync/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ node tools/integrations-catalog-sync/generate-readme.mjs
2424
Optional tuning:
2525

2626
- `SYNC_CONCURRENCY` (default `2`) — concurrent fetches. Higher values trigger API rate limits.
27+
- `SYNC_MIN_INTERVAL_MS` (default `100`) — minimum interval between HTTP requests, enforced globally across all workers. Paces the sync to stay under the API throttle. Increase if you still see 429s; decrease (or set `0`) if the backend is raised later.
2728

2829
## Safety guardrails
2930

tools/integrations-catalog-sync/sync.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,27 @@ function sleep(ms) {
5656
return new Promise((resolve) => setTimeout(resolve, ms));
5757
}
5858

59+
const MIN_REQUEST_INTERVAL_MS = parseInt(process.env.SYNC_MIN_INTERVAL_MS || "100", 10);
60+
let nextAllowedAt = 0;
61+
62+
async function pace() {
63+
const now = Date.now();
64+
if (now < nextAllowedAt) await sleep(nextAllowedAt - now);
65+
nextAllowedAt = Date.now() + MIN_REQUEST_INTERVAL_MS;
66+
}
67+
5968
async function fetchJson(path, maxRetries = 5) {
6069
let lastError;
6170
for (let attempt = 0; attempt < maxRetries; attempt++) {
71+
await pace();
6272
const res = await fetch(`${API_BASE}${path}`, { headers: HEADERS });
6373
if (res.ok) return res.json();
6474
const text = await res.text();
6575
if ((res.status === 429 || res.status >= 500) && attempt < maxRetries - 1) {
6676
const retryAfter = parseInt(res.headers.get("retry-after") || "0", 10);
6777
const delay = retryAfter > 0 ? retryAfter * 1000 : Math.min(1000 * Math.pow(2, attempt), 15000);
6878
lastError = new Error(`${path} HTTP ${res.status}: ${text.slice(0, 200)}`);
79+
console.warn(` ${path}${res.status}, backing off ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
6980
await sleep(delay);
7081
continue;
7182
}

0 commit comments

Comments
 (0)