Skip to content

Rate-Limited API Calls

Call external APIs while respecting rate limits using throttle and concurrency controls.

ts
const callStripeApiTask = taskora.task("stripe-api-call", {
  concurrency: 5, // max 5 parallel API calls
  timeout: 30_000,
  retry: {
    attempts: 3,
    backoff: "exponential",
    delay: 2000,
  },
  handler: async (data: { method: string; params: Record<string, unknown> }, ctx) => {
    const res = await fetch(`https://api.stripe.com/v1/${data.method}`, {
      method: "POST",
      headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET}` },
      body: new URLSearchParams(data.params as any),
      signal: ctx.signal,
    })

    if (res.status === 429) {
      const retryAfter = Number(res.headers.get("retry-after")) * 1000
      throw ctx.retry({ delay: retryAfter, reason: "Stripe rate limit" })
    }

    if (!res.ok) throw new Error(`Stripe API error: ${res.status}`)

    return await res.json()
  },
})

// Dispatch with throttle — max 25 requests per second
callStripeApiTask.dispatch(
  { method: "customers", params: { email: "user@example.com" } },
  { throttle: { key: "stripe", max: 25, window: "1s" } },
)

// Per-customer concurrency limit
callStripeApiTask.dispatch(
  { method: "charges", params: { customer: "cus_123", amount: "1000" } },
  {
    concurrencyKey: `stripe:cus_123`,
    concurrencyLimit: 1, // one charge at a time per customer
  },
)

Pattern: Automatic Rate Limit Detection

ts
handler: async (data, ctx) => {
  const res = await callApi(data)

  if (res.status === 429) {
    // Use the API's retry-after header for precise backoff
    const delay = Number(res.headers.get("retry-after")) * 1000 || 5000
    throw ctx.retry({ delay })
  }

  return res.data
}

The ctx.retry({ delay }) override bypasses the configured backoff strategy and uses the API's suggested retry delay instead.

Released under the MIT License.