toomanyrequests
You sent more requests with one API key in the current minute than your plan allows. The API rejects the extra requests with HTTP 429 until the minute ends. Nothing is wrong with the requests themselves: send the same request again after Retry-After seconds and it succeeds. 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 |
| Headers | Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining: 0, X-RateLimit-Reset |
Example response
We captured this response from the API code in a test run: 12 requests in a row with one key on the free plan. Requests 1 to 10 returned 200. Requests 11 and 12 returned this:
HTTP/1.1 429 Too Many Requests Content-Type: application/json Retry-After: 6 X-RateLimit-Limit: 10 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1792041660 { "error": { "type": "rate_limit_error", "code": "too_many_requests", "message": "Rate limit exceeded. Please retry after 6 seconds.", "retryable": true }, "request_id": "9e72086b-19e9-4c71-9381-834273c46d9d" }
Retry-After: 6 is not a penalty. The run happened 6 seconds before the minute changed, and that is all the time left in the window. X-RateLimit-Reset is the same moment as a Unix timestamp.
How the window works
- Per API key. Each key has its own counter. Two services that share one key share one limit.
- One fixed window per clock minute. The counter starts at zero at second 0 of each minute (UTC). It does not slide. So
Retry-Afteris between 1 and 60 seconds, and it is often short. - Every
/v1/request counts. Renders, cache hits,GET /v1/devices,GET /v1/presets, batch status polls and the rejected 429s all add one. - Batches count once. One
POST /v1/batchwith many URLs is one request against this limit. Its size is capped separately; see the batch endpoint.
The per-minute number for each plan is on the rate limits reference.
toomanyrequests or concurrent_limit?
Both use status 429. too_many_requests counts requests started in the current minute and clears when the minute ends. concurrent_limit is about renders in progress at the same moment. If you cannot read error.code (see the SDK note below), use Retry-After: it is set on too_many_requests.
Common causes
- A loop with no spacing that sends a list of URLs as fast as the API answers. Cache hits answer in milliseconds, so a warm cache makes this worse, not better.
- Several workers on one key, each one under the limit, together over it.
- Polling. A dashboard or job that checks batch status every second uses most of a small plan's minute on its own.
- Retries without a wait, which burn the rest of the window.
How to fix it
- On a 429, wait
Retry-Afterseconds, plus a little random jitter, then send the same request again. - Space your requests: one request every
60 / limitseconds per key keeps you under the limit. The rate limits page has a pacing script that reads the limit from the response headers. - Send lists of URLs with the batch endpoint instead of one request per URL.
- Give each independent service its own API key, so one busy job cannot starve the others.
- If you still hit the limit at normal traffic, compare plans on the pricing page.
Node.js SDK
Base the check on err.httpStatus and err.retryAfter. In SDK version 1.1.0, err.code does not carry the API's code for errors from the server, so do not branch on it.
import { Client, TakeOptions, RenderScreenshotError } from 'renderscreenshot'; import { writeFile } from 'node:fs/promises'; const client = new Client(process.env.RS_API_KEY); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function takeWithRateLimit(options, maxWaits = 3) { for (let waits = 0; ; waits++) { try { return await client.take(options); } catch (err) { const limited = err instanceof RenderScreenshotError && err.httpStatus === 429; if (!limited || waits === maxWaits) throw err; // Retry-After is the number of seconds to the next window. The jitter stops // several workers from all retrying in the same second. const seconds = (err.retryAfter ?? 60) + Math.random(); console.warn(`Rate limited, waiting ${seconds.toFixed(1)} s`); await sleep(seconds * 1000); } } } const urls = ['https://example.com/', 'https://example.com/a', 'https://example.com/b', 'https://example.com/c', 'https://example.com/d']; for (const [i, url] of urls.entries()) { const image = await takeWithRateLimit(TakeOptions.url(url).preset('og_card')); await writeFile(`card-${i}.png`, image); } console.log(`Saved ${urls.length} cards`);
Python SDK
The same rule in Python: e.http_status and e.retry_after are the reliable fields.
import os import random import time from renderscreenshot import Client, RenderScreenshotError, TakeOptions client = Client(os.environ['RS_API_KEY']) def take_with_rate_limit(options, max_waits=3): for waits in range(max_waits + 1): try: return client.take(options) except RenderScreenshotError as e: if e.http_status != 429 or waits == max_waits: raise # Seconds to the next window, plus jitter. time.sleep((e.retry_after or 60) + random.random()) for i in range(5): image = take_with_rate_limit(TakeOptions.url(f'https://example.com/{i}').preset('og_card')) with open(f'card-{i}.png', 'wb') as f: f.write(image) print('Saved 5 cards')
In Ruby, rescue RenderScreenshot::RateLimitError and sleep e.retry_after. The error code reference has that listing.
Tested on 2026-10-15. The example response came from the Rails API code in its test environment. Both listings ran against a local stub with the same body and headers and a limit of 3 requests per 2-second window: Node 22.18 with renderscreenshot 1.1.0, and Python 3.14 with renderscreenshot 1.0.0. In each run one request hit the limit, the script waited about 2 seconds, and all 5 images were saved. For the test, the constructor received the stub's base URL; nothing else changed.
Related
- Rate limits: per-plan limits, headers and a pacing script.
- Bulk screenshots: capture thousands of URLs without hitting 429.
- Error handling and retries: one retry policy for every error code.
Last verified 2026-10-15 against the API source.