- Published on
Retry 429 with backoff + pool
A FIFO pool keeps you under your share of the account concurrency limit. The retry wrapper handles the spikes you cannot predict.
netsuite-client.js
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
export function createPool(size) { let active = 0 const waiting = [] const next = () => { if (active >= size || waiting.length === 0) return active++ const { task, resolve, reject } = waiting.shift() task() .then(resolve, reject) .finally(() => { active-- next() }) } return (task) => new Promise((resolve, reject) => { waiting.push({ task, resolve, reject }) next() })}
const RETRYABLE = new Set([429, 502, 503, 504])
export async function withRetry(doRequest, { maxAttempts = 6, baseMs = 500, capMs = 30_000 } = {}) { for (let attempt = 1; ; attempt++) { let res try { res = await doRequest() } catch (networkError) { if (attempt >= maxAttempts) throw networkError } if (res && !RETRYABLE.has(res.status)) return res if (res && attempt >= maxAttempts) return res
const retryAfter = Number(res?.headers.get('retry-after')) * 1000 const ceiling = Math.min(capMs, baseMs * 2 ** attempt) await sleep(retryAfter || Math.random() * ceiling) }}Usage:
const pool = createPool(4)const results = await Promise.all( ids.map((id) => pool(() => withRetry(() => fetch(urlFor(id), { headers: authHeaders() })))))Build auth headers inside doRequest, so every attempt gets a fresh TBA nonce or a non-expired token. Only retry writes that are idempotent. Interactive explanation: NetSuite concurrency limits.