concurrent_limit
The API returns concurrent_limit when you start a render while your plan's maximum number of renders is already in progress. It is a timing problem, not a request problem: the same request succeeds as soon as one of your running renders finishes. This page is part of the error code reference.
| HTTP status | 429 Too Many Requests |
error.type |
rate_limit_error |
error.retryable |
true |
| Credits used | 0 |
Example response
{ "error": { "type": "rate_limit_error", "code": "concurrent_limit", "message": "Too many concurrent requests. Maximum is 5.", "retryable": true, "docs": "https://docs.renderscreenshot.com/errors/concurrent_limit" }, "request_id": "req_abc123" }
concurrentlimit or toomany_requests?
Both codes use HTTP 429, so read error.code before you decide how long to wait.
| Code | What it counts | When a retry can succeed |
|---|---|---|
too_many_requests |
Requests started in the current one-minute window | When the window resets (Retry-After seconds) |
concurrent_limit |
Renders running at this moment | When any one of your running renders ends, often within a few seconds |
Waiting a full minute after concurrent_limit is safe but slow. A short wait with jitter is enough.
Common causes
Promise.allover a list of URLs. Every capture starts at once. Ten URLs on a plan with a lower limit means some of them fail at once.- Several workers or servers on one account. The limit comes from your plan, so count every process that sends requests with your keys, including cron jobs and preview deployments.
- Slow pages. A page that waits for
networkidleor a longwait.timeoutholds its slot for longer, so the same request rate needs more slots. - Retries that add load. A retry loop with no delay re-sends at once and keeps every slot full.
How to fix it
- Read your limit from
GET /v1/usage(rate_limits.concurrent_limit), or from the rate limits page. - Run at most that many captures at once, across all your processes.
- On
concurrent_limit, wait a short, random time and retry. UseRetry-Afterif the response has it. - For lists of URLs, consider the batch endpoint. It schedules the renders for you within your plan's batch limits.
A worker pool that respects the limit
This script asks the API for your limit, then runs that many workers. Each worker takes the next URL when it is free, so the number of renders in flight never goes above the limit. It still handles concurrent_limit, because another process on the same account can take a slot.
// capture-pool.mjs (Node 22+, no dependencies) const API = process.env.RS_BASE_URL ?? 'https://api.renderscreenshot.com'; const headers = { Authorization: `Bearer ${process.env.RS_API_KEY}` }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // Ask the API how many renders your plan may run at the same time. async function concurrentLimit() { const res = await fetch(`${API}/v1/usage`, { headers }); if (!res.ok) throw new Error(`GET /v1/usage failed with HTTP ${res.status}`); const usage = await res.json(); return usage.rate_limits?.concurrent_limit ?? 1; } async function capture(url, attempt = 1) { const res = await fetch(`${API}/v1/screenshot`, { method: 'POST', headers: { ...headers, 'Content-Type': 'application/json' }, body: JSON.stringify({ url, preset: 'og_card' }), }); if (res.ok) return Buffer.from(await res.arrayBuffer()); const body = await res.json().catch(() => null); if (body?.error?.code === 'concurrent_limit' && attempt < 5) { // A slot frees up when one of your renders finishes: wait briefly, with jitter. const retryAfter = Number(res.headers.get('retry-after')); await sleep(retryAfter > 0 ? retryAfter * 1000 : Math.random() * 500 * attempt); return capture(url, attempt + 1); } throw new Error(`${res.status} ${body?.error?.code}: ${body?.error?.message} (${body?.request_id})`); } // Run at most `limit` captures at once. Each worker takes the next URL when it is free. async function captureAll(urls, limit) { const results = new Array(urls.length); let next = 0; const worker = async () => { while (next < urls.length) { const i = next++; results[i] = await capture(urls[i]).catch((error) => error); } }; await Promise.all(Array.from({ length: limit }, worker)); return results; } const limit = await concurrentLimit(); const urls = Array.from({ length: 8 }, (_, i) => `https://example.com/?page=${i}`); const results = await captureAll(urls, limit); const failed = results.filter((r) => r instanceof Error); console.log(`limit ${limit}: ${results.length - failed.length} captured, ${failed.length} failed`); if (failed.length) process.exitCode = 1;
With the Ruby SDK
The Ruby SDK raises RenderScreenshot::RateLimitError for every 429, and e.code tells the two cases apart:
require 'renderscreenshot' client = RenderScreenshot.client(ENV.fetch('RS_API_KEY')) options = RenderScreenshot::TakeOptions.url('https://example.com').preset('og_card') attempts = 0 begin attempts += 1 image = client.take(options) rescue RenderScreenshot::RateLimitError => e raise if attempts >= 5 # concurrent_limit clears when one of your renders ends; too_many_requests at the next minute. sleep(e.code == 'concurrent_limit' ? rand(0.5..2.0) : (e.retry_after || 60)) retry end File.binwrite('shot.png', image)
Tested on 2026-10-15 with Node 22.18 and with Ruby 4.0.7 plus renderscreenshot gem 1.0.0, against a local mock that returns this error envelope and enforces a limit of 2. With the pool sized from /v1/usage, no request hit the limit. With the pool deliberately set 2 above the limit, the retries absorbed every concurrent_limit and all 8 captures succeeded.
Related
- Error handling and retries for one retry policy across all codes.
- Rate limits for per-plan limits and the rate limit headers.