Rate Limits
Repull applies three request limits at once. They protect different things and they clear at different times, so the first job when you see a 429 is to work out which one you hit — the answer changes what you should do next.
The three limits
| Limit | Window | Protects | Error |
|---|---|---|---|
| Per-key burst | 60 seconds, per API key | Our service, from a traffic spike | rate_limit_exceeded / per_api_key |
| Daily requests | UTC calendar day | You, from your own runaway loop | daily_limit_exceeded |
| Monthly requests | Calendar month | Your plan allowance | rate_limit_exceeded / monthly |
| Daily AI requests | UTC calendar day, AI operations only | Your plan's AI allowance | rate_limit_exceeded / daily_ai |
All four can fire on the same request. Whichever you hit first is the one you are told about.
Limits by plan
| Plan | Per-key burst | Daily requests | Monthly requests | Daily AI |
|---|---|---|---|---|
| Free | 600 / min | 2,000 | 1,000 | 10 |
| Starter | 600 / min | 25,000 | 100,000 | 1,000 |
| Custom | 600 / min | Unlimited | Unlimited | Unlimited |
On the free plan the monthly allowance is the smaller number, so it is the one you will meet first; the daily cap is a backstop. Any of these can be raised for an individual account — ask support. Unverified accounts are held to 100 requests/minute on sk_test_ keys until the email on the account is confirmed.
Why there is a daily cap
A per-minute limit and a monthly quota between them leave a real gap, and it is the gap most integration bugs fall into.
A loop is not a burst. A client that retries forever without backoff settles into a steady rate that the per-minute limiter is perfectly happy with — the limiter paces the loop rather than stopping it. The monthly quota does eventually stop it, but only after hours, and it then holds your integration down until the next billing month.
The daily cap is the middle. It notices within the hour, and it forgives at the next UTC midnight. It is sized so that no legitimate day of traffic we have ever served would have been refused — if it fires, something is almost certainly looping.
Response headers
Every response carries both windows, so you can slow down before anything breaks:
| Header | Description |
|---|---|
X-RateLimit-Limit | Requests allowed in the per-key burst window |
X-RateLimit-Remaining | Requests left in the current burst window |
X-RateLimit-Reset | When the burst window rolls over |
X-RateLimit-Limit-Daily | Your daily request cap (absent when unlimited) |
X-RateLimit-Remaining-Daily | Requests left today — the early-warning signal |
X-RateLimit-Reset-Daily | ISO timestamp of the next UTC midnight |
Retry-After | Seconds to wait (429 responses only) |
Telling the 429s apart
Branch on error.code first, then error.scope. The three cases want three different responses from your code:
| You see | It means | Do |
|---|---|---|
rate_limit_exceededscope: per_api_key | You are going too fast right now. | Back off and retry. Retry-After is seconds, usually small. |
daily_limit_exceeded | You are probably looping. | Stop. Read top_operation, fix the call site, then resume. Retrying will not help. |
rate_limit_exceededscope: monthly | You are out of plan requests for the month. | Upgrade, or wait for the new billing month. |
rate_limit_exceededscope: daily_ai | Out of AI calls for today only. | Non-AI endpoints still work. Resume tomorrow, or upgrade. |
Full recovery guides: daily_limit_exceeded and rate_limited.
Seeing your usage
GET /v1/usage/tier is the quota meter — cheap enough to poll, and it reports every window with its own reset time.
curl https://api.repull.dev/v1/usage/tier \
-H "Authorization: Bearer sk_live_YOUR_KEY"
{
"tier": "starter",
"limits": {
"dailyRequests": 25000,
"monthlyRequests": 100000,
"dailyAiRequests": 1000,
"dynamicPricingListings": 5
},
"used": { "daily": 18000, "monthly": 40000, "dailyAi": 3 },
"remaining": { "daily": 7000, "monthly": 60000, "dailyAi": 997 },
"dailyResetsAt": "2026-05-02T00:00:00.000Z",
"resetsAt": "2026-06-01T00:00:00.000Z"
}GET /v1/usage/summaryadds a per-operation breakdown and a daily timeline. It is the fastest way to answer "where did my day go?" — sort the breakdown by request count and a runaway loop is usually the first row by a wide margin.
curl "https://api.repull.dev/v1/usage/summary?range=7d" \ -H "Authorization: Bearer sk_live_YOUR_KEY"
Getting warned before it bites
At 80% of your daily cap Repull emits a usage.quota.warning webhook — once per day, never once per request. It carries the same topOperation diagnosis the 429 would, so you can catch a loop while everything is still working. Subscribe to it and you should never meet the breaker itself.
{
"type": "usage.quota.warning",
"data": {
"scope": "daily_requests",
"tier": "starter",
"used": 20000,
"limit": 25000,
"percentUsed": 80,
"remaining": 5000,
"resetsAt": "2026-05-02T00:00:00.000Z",
"topOperation": {
"operationId": "replay_webhook_delivery",
"requestCount": 17000,
"sharePercent": 85
}
}
}Handling 429s in code
Back off on the burst limit. Do not back off on the daily cap — treat it as a bug report about your own code and stop:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
const body = await res.clone().json();
// The daily circuit breaker. Retrying re-runs the loop that caused it.
if (body.error?.code === 'daily_limit_exceeded') {
const top = body.error.top_operation;
throw new Error(
`Daily request cap hit (${body.error.used}/${body.error.limit}).` +
(top ? ` Check ${top.operation_id} — ${top.share_percent}% of today's traffic.` : '')
);
}
// Monthly quota: waiting will not help either; this needs a plan change.
if (body.error?.scope === 'monthly') {
throw new Error('Monthly request quota exhausted — upgrade the plan.');
}
// Burst limit: this is the one worth waiting out.
const retryAfter = parseInt(res.headers.get('Retry-After') || '1', 10);
const backoff = retryAfter * Math.pow(2, attempt) + Math.random();
await new Promise(r => setTimeout(r, backoff * 1000));
}
throw new Error('Max retries exceeded');
}Raising a limit
Every limit on this page can be set per account, independently of the plan — including the daily cap on its own, without changing your monthly quota. If your traffic is genuine and growing, contact support with your workspace id and a rough shape of the load. Before you do, check /v1/usage/summary: it is worth being sure the volume is work you meant to do.