Rate limits
RenderScreenshot has two kinds of limits. A per-minute limit caps how many API requests one account can send, with all of its keys together. Batch limits cap how many URLs one batch can hold and how many of them render at the same time. Both depend on your plan. Credits, the monthly number of renders, are a separate allowance: see credits and billing. This page is part of the API documentation.
Limits by plan
| Plan | Requests per minute, per account | Requests per hour at full pace | URLs per batch | Batch URLs rendered at once |
|---|---|---|---|---|
| Free | 10 | 600 | 5 | 2 |
| Starter | 60 | 3,600 | 20 | 5 |
| Growth | 120 | 7,200 | 50 | 10 |
| Scale | 200 | 12,000 | 100 | 20 |
These are the values the API enforces, taken from the plan configuration on 2026-10-15. Every response to your key reports the account limit in the X-RateLimit-Limit header, so read that in code rather than copying this table. Prices and credits are on the pricing page.
How each limit works
Requests per minute. The API counts requests per account in a fixed window that starts at second 0 of every minute (UTC). All keys of the account share this counter: live keys sent as a Bearer token or api_key, and signed URLs made with the account's signing keys. The limit stays per account when you use workspaces: keys of all workspaces share the same counter. A request with a key that is not valid does not count against any account; it is counted per client IP at the Free limit. The window does not slide, so the counter is back to zero at the next full minute however many requests you sent. Every authenticated request to /v1/ counts: renders, cache hits, GET /v1/devices, GET /v1/presets, batch status checks, and requests that were themselves rejected with 429.
URLs per batch. The largest urls (or requests) list one POST /v1/batch may carry. A larger batch is rejected before any work starts with HTTP 403 and the code batch_size_exceeded. Split it, or use a plan with a larger batch size. The whole batch counts as one request against the per-minute limit.
Batch URLs rendered at once. The number of URLs from one batch that render in parallel. The rest wait in the queue. A batch larger than this number is not an error; it only takes longer.
What happens at each limit
| Limit reached | Status | error.code |
What to do |
|---|---|---|---|
| Requests per minute | 429 | too_many_requests |
Wait Retry-After seconds, then retry |
| No free browser for a render | 429 | concurrent_limit |
Wait Retry-After seconds, then retry |
| URLs per batch | 403 | batch_size_exceeded |
Split the batch |
| Monthly credits | 403 | insufficient_credits |
Wait for the monthly reset or change plan |
A rejected request never uses a credit.
Rate limit headers
API responses to an authenticated key carry three rate limit headers; a request rejected early by validation can omit them. A 429 response adds Retry-After.
X-RateLimit-Limit: 60 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1792041660 Retry-After: 6
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Your account's requests per minute, shared by all its keys. Read it instead of hard-coding your plan's number. |
X-RateLimit-Remaining |
Requests left in the current window for the account, after the requests of all its keys. Treat it as a hint and pace with the limit and Retry-After. |
X-RateLimit-Reset |
Unix timestamp of the next window, which is the start of the next minute |
Retry-After |
On 429 only. For too_many_requests: seconds until the next window, from 1 to 60. For concurrent_limit: a short fixed wait |
Pace requests before you hit the limit
Handling a 429 after the fact works, but it is cleaner not to get one. With a fixed one-minute window, one request every 60 / limit seconds never sends more than limit requests in any window. This script learns the limit from X-RateLimit-Limit on the first response, spaces every later request by that gap, and still waits out a 429 if another process uses the same account.
// paced-capture.mjs (Node 22+, no dependencies) // Spaces requests so one account stays under its per-minute limit. // Call pacedPost from one loop at a time: it does not coordinate parallel callers. import { writeFile } from 'node:fs/promises'; const API = process.env.RS_BASE_URL ?? 'https://api.renderscreenshot.com'; const WINDOW_MS = 60_000; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); let gap = 0; // milliseconds between requests, learned from X-RateLimit-Limit let nextAt = 0; // earliest time the next request may start async function pacedPost(body) { for (;;) { if (nextAt > Date.now()) await sleep(nextAt - Date.now()); const startedAt = Date.now(); const res = await fetch(`${API}/v1/screenshot`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.RS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); const limit = Number(res.headers.get('x-ratelimit-limit')); if (limit > 0) gap = Math.ceil(WINDOW_MS / limit); nextAt = startedAt + gap; if (res.status !== 429) return res; // Another process used the same account. Wait for the next window, then continue. nextAt = Date.now() + (Number(res.headers.get('retry-after')) || 60) * 1000; } } const urls = Array.from({ length: 7 }, (_, i) => `https://example.com/?item=${i}`); for (const [i, url] of urls.entries()) { const res = await pacedPost({ url, preset: 'og_card' }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(`${res.status} ${body.error?.code}: ${body.error?.message}`); } await writeFile(`card-${i}.png`, Buffer.from(await res.arrayBuffer())); console.log(`${new Date().toISOString()} saved card-${i}.png`); }
If several processes use the same account, give each one a share of the limit (set WINDOW_MS to 60_000 * processes). A separate key for each process does not help: all keys of the account share one limit. For a retry loop built on the SDKs instead, see toomanyrequests.
Tested on 2026-10-15 with Node 22.18 against a local stub that enforces 3 requests per 2-second window, with WINDOW_MS set to 2,000 for the test. Seven requests ran 0.67 seconds apart with no 429. In a second run, three other requests used the same limit first; the script got one 429, waited for the next window and saved all seven images.
Use fewer requests
- Send lists as batches. One batch request replaces up to your plan's batch size in single requests. The bulk screenshots guide covers lists of thousands of URLs.
- Use webhooks instead of polling. A loop that checks batch status every second uses 60 requests per minute on its own. A webhook uses none.
- Serve images from the CDN. The
X-Cache-URLresponse header is a public CDN link. Embed that link in your pages, and a page view does not call the API. A signed API URL in an<img>tag is different: each view that reaches the API counts toward the limit of the account that owns the signing key. - Expect cache hits to count.
cache.available: truesaves credits on repeat renders, but each call is still a request.
Higher limits
If you need more requests per minute than the largest plan allows, email [email protected] with your expected volume and your use case.
See also
- toomanyrequests: the 429 response and SDK retry code
- Error codes: every error code with its status
- Caching:
X-Cache-URLand cache lifetimes - Error handling and retries: which errors to retry and how long to wait
Last verified 2026-10-15 against the API source.