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-URL response 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: true saves 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

Last verified 2026-10-15 against the API source.

Related in this topic

Was this page helpful?