API rate limits explained: what 429 means on Qrop
By Qrop Team ·
A 429 response is easy to misread as a billing issue: something failed, so the instinct is to check the credit balance. On Qrop, a 429 means something narrower and more mechanical: requests arrived faster than your plan's rate limit allows, full stop. Nothing was charged, nothing about your subscription changed, and the fix has nothing to do with buying more credits.
This guide covers exactly what the two rate-limit windows check, how the Retry-After header tells you precisely how long to wait, the separate (and much stricter) limit that guards against API key guessing, and the practical patterns that keep a real integration comfortably under its ceiling instead of bouncing off it.
If you're specifically trying to push volume through the batch endpoint, the batch generation guide covers parallelizing calls within these same limits in more depth. This article is the rate-limit mechanics on their own, independent of which endpoint you're calling.
What actually trips a 429
Every API key has two rate-limit windows checked in parallel on every request: a per-second limit and a per-minute limit. Both have to pass for the request to proceed. Whichever one you exceed first is the one that rejects the request, a short, sharp burst usually trips the per-second window; a steady stream that never bursts but never lets up can still trip the per-minute window instead.
This is a deliberate design, not an accident: a per-minute limit alone would let a client send its entire minute's allowance in the first second, which is exactly the kind of spike that causes latency problems for other customers sharing the same infrastructure. The per-second window exists specifically to smooth that out.
Reading the Retry-After header
A 429 response always includes a Retry-After header, telling you how many seconds to wait before trying again. It reflects whichever of the two windows resets later, so it's always a safe number to wait out. Guessing and retrying early just costs you another rejected request; waiting the full duration guarantees the next attempt lands in a fresh window.
There's no separate error code or message to distinguish which of the two windows caused the rejection, and in practice it doesn't matter: the fix is the same either way, wait for Retry-After, then proceed normally. Building retry logic against a fixed short delay (say, always retrying after 1 second) will work most of the time but isn't correct, since a per-minute rejection can carry a Retry-After well past a single second.
Limits by plan
Rate limits scale with your API plan, alongside included credits, since a higher tier is meant to support both more total volume and a higher burst rate. GET /api/v1/usage returns your current plan's exact limits in its rateLimit field, so there's never a need to hardcode these numbers against a plan name in your own integration.
- Starter: 5 requests/second, 100 requests/minute.
- Pro: 15 requests/second, 400 requests/minute.
- Growth: 25 requests/second, 1,000 requests/minute.
- Enterprise: 100 requests/second, 5,000 requests/minute (and negotiable further for genuinely unusual volume).
A separate, stricter limit: failed authentication
There's a second, unrelated rate limit worth knowing about: repeated failed authentication attempts from one IP address are capped at 10 per minute, regardless of plan. This exists to slow down anyone guessing at API keys, not to throttle legitimate traffic, and it's IP-based rather than key-based since a would-be attacker doesn't have a valid key to be identified by in the first place.
In practice this only bites a real integration by accident: a misconfigured client retrying a stale or rotated key in a tight loop can trip this limit and start seeing 429s that look identical to a quota rejection but are actually about the key itself. If you rotate a key via /api/v1/keys/rotate, make sure every server or process using the old key is updated within its 24-hour grace period, rather than left retrying a key that's about to stop working.
Rate limits don't touch your credits
A 429 is rejected before your request reaches the generation or credit-consumption logic at all, so it never draws down included credits, purchased credits, or overage. There's no cleanup needed on your end beyond retrying after the wait: no partial charge, no reconciliation, nothing to check against /api/v1/usage afterward.
This is worth internalizing because it changes how you should think about a 429 operationally: it's purely a pacing signal, not a resource or billing event. Treat it the way you'd treat a database connection pool being briefly saturated: back off, retry, move on, not a partial failure to investigate.
Designing an integration that rarely sees a 429
- Check GET /api/v1/usage once at startup (and periodically) rather than assuming a plan's limits, since an upgrade or downgrade changes them without any code change on your end.
- Cap your own client-side concurrency at, or just under, your plan's requests-per-second figure, rather than firing every request the moment it's ready and hoping the server absorbs the burst.
- Prefer /api/v1/generate/batch over a tight loop of single calls when you already know every destination up front, one batch request of 25 items counts as one request against your rate limit, not 25.
- On a 429, wait the exact Retry-After duration before the next attempt, don't guess a shorter delay or retry immediately in a hot loop, that just stacks up more rejected requests against the same window.
- If your real, sustained volume is bumping against your plan's per-minute ceiling on a normal day rather than only during rare bursts, that's a signal to upgrade a tier rather than to keep tuning retry logic around a limit you've simply outgrown.
A worked example: smoothing a bursty import job
Say a Pro-plan integration (15 requests/second, 400/minute) needs to import 800 QR codes as fast as possible. Firing all 800 single-item requests as fast as the client can loop would blow past 15/second almost immediately and spend most of the job retrying 429s rather than making progress.
The fix is two changes, not one: first, switch to /api/v1/generate/batch and chunk the 800 items into 32 batches of 25, since batch calls count as one request each regardless of item count. Second, cap concurrency at roughly 12 to 13 concurrent batch calls (comfortably under the 15/second ceiling, leaving headroom for jitter) rather than firing all 32 at once. At that pace, the whole import finishes in under 3 seconds of request time without a single 429, compared to a naive single-item loop that would spend most of its runtime waiting out Retry-After headers.
Frequently asked questions
Does a 429 response mean I've run out of credits?
No. A 429 is a rate-limit rejection, purely about request pace, and is entirely separate from your credit balance or subscription status. Running out of credits or an inactive subscription returns 402 instead. The two are unrelated and never overlap on the same response.
How long should I wait before retrying after a 429?
Exactly as long as the Retry-After header says, in seconds. It already accounts for whichever of the per-second or per-minute windows caused the rejection and will reset later, so there's no need to guess or add your own buffer beyond it.
Do rate limits apply per API key or per account?
Per API key. If your account holds multiple keys, each is rate-limited independently against your plan's limits, which is useful for isolating one integration's traffic from another's rather than having them compete for the same ceiling.
Will a batch request with 25 items count as 25 requests against my rate limit?
No, one batch call counts as a single request regardless of how many items are inside it, up to the 25-item cap. This is one of the main reasons batching is worth using at volume: it reduces your actual request count against the rate limit, not just your bookkeeping overhead.
I'm on Starter and keep hitting 429s during normal use, what should I do?
First confirm it's a genuine rate-limit issue by checking GET /api/v1/usage against your actual request pattern, a burst of more than 5 requests in the same second is enough to trigger it even well under your monthly credit allocation. If your real, sustained traffic needs a higher burst rate rather than just smoother client-side pacing, moving to Pro (15/second) or higher is the direct fix, upgrading changes the limit immediately once the plan change takes effect.
Related guides
Ready to create your QR code?
Generate a free QR code in seconds: custom colours, logos, and PNG, SVG or PDF output.
Open the generator