June 20, 2026 · 6 min read · Industry
Rate-limiting a B2B travel API: per-minute vs per-month, and why both.
Single-scale rate limits are a residual habit from consumer APIs. B2B workloads need two: a per-minute cap to shield production from runaway retries, and a per-month cap to make pricing tiers predictable. Most travel APIs ship one or the other. We ship both.
Why per-minute matters
A customer's job queue gets stuck. Their retry loop fires every 50ms against /v1/cities for the same query. With a monthly-only quota, they burn through 50,000 calls in fifteen minutes, hit the wall, and your support inbox lights up at 3am. Worse, the burst saturates a shard and degrades latency for every other customer on the same node.
Per-minute caps are a circuit breaker. If a client exceeds 600 requests in 60 seconds on the Starter tier, we 429 them; the retry loop stops mattering because every retry returns immediately. The customer's runbook fires before the monthly quota is touched.
Why per-month matters
Per-minute alone makes pricing unpredictable. A Starter customer at 600 req/min can theoretically pull 25.9M calls/month if their workload is steady. That isn't what they're paying for. The monthly cap is the contract: 100k on Free, 500k on Starter, 5M on Growth, custom on Scale. Predictable pricing for the customer, predictable cost for us.
The headers we return
Every response includes both scales. Industry-standard names where they exist; we add the second scale on parallel keys.
HTTP/1.1 200 OK
X-RateLimit-Limit-Minute: 600
X-RateLimit-Remaining-Minute: 487
X-RateLimit-Reset-Minute: 32
X-RateLimit-Limit-Month: 500000
X-RateLimit-Remaining-Month: 412305
X-RateLimit-Reset-Month: 1719446400
Retry-After: 32
The Retry-After value is the tighter of the two — whichever bucket the client should respect to make progress. A client doesn't need to parse all six headers; reading Retry-After on a 429 is sufficient for a correct backoff.
The 429 body
HTTP/1.1 429 Too Many Requests
Retry-After: 32
Content-Type: application/json
{
"error": "rate_limit_exceeded",
"scope": "minute",
"limit": 600,
"reset_seconds": 32,
"message": "Burst limit reached. Retry in 32 seconds, or upgrade tier."
}
The scope field is the load-bearing part. A client whose
minute bucket is full but month bucket has 90% remaining handles
differently from a client whose monthly quota is exhausted. The first
backs off; the second escalates to a human or upgrades the tier.
Backoff that respects both scales
Naive exponential backoff starts at 1 second and doubles. Against a monthly quota exhaustion this is wrong — the client retries 30 times over an hour before realizing the quota is gone. Honour Retry-After when present, and stop on monthly scope:
async function call(url) {
const r = await fetch(url, { headers: { Authorization: API_KEY } });
if (r.status !== 429) return r;
const body = await r.json();
if (body.scope === "month") {
throw new QuotaExhausted(body.message);
}
const delay = parseInt(r.headers.get("Retry-After"), 10) * 1000;
await sleep(delay);
return call(url);
}
What happens at the boundaries
Per-minute window: sliding, computed on a 60-second tail. Not a fixed minute boundary. A client that fires 600 calls at 12:00:30 and another 600 at 12:01:30 is fine. A client that fires 1,200 calls in the same second is not.
Per-month window: calendar month in UTC. Resets at 00:00 UTC on the 1st of each month. Not 30 days from signup, not the customer's timezone — UTC. We surface the reset epoch in the header for clients that want to render their own usage chart.
Industry context
Stripe ships per-minute and per-second on writes, no monthly quota (because they bill per transaction, not per call). Twilio ships per-second on send, monthly billing per message. AWS ships per-second burst with token-bucket credit accumulation. Google Maps Platform ships QPS plus monthly billing thresholds.
For a travel data API, the two scales we care about are minute and month. Per-second is too tight for typical travel workloads (most queries are not user-blocking). Per-day is too coarse to be useful against runaway retries. Minute and month is the sweet spot.