ScreenshotNeo

BlogHow-to

URLbox Screenshot API Rate Limits and Retries for Bulk Captures

Handle URLbox screenshot API limits safely: inspect quota headers, honor Retry-After, pace bulk jobs, and distinguish API throttling from target-site blocks.

By the ScreenshotNeo team4 October 202611 min read

Direct answer: URLbox does not document one universal screenshot API rate limit. Your per-minute allowance depends on your plan. Check the rate-limit headers on responses, pace requests to your account’s actual quota, and when URLbox returns an API-level 429, wait for the Retry-After duration before sending more requests. For large capture jobs, use a queue with bounded concurrency; async rendering changes how you collect results, not the account’s quota.

This guide covers bulk scheduling, sync and async choices, retry settings, working Python and Node.js examples, and how to tell API throttling apart from a target website blocking a render. URLbox’s documented retry controls require Ultra or above. Confirm the current plan and options in the URLbox API reference and rate-limits documentation.

1. Find your account’s actual rate limit

URLbox documents request limits per minute, with the limit dependent on the customer’s plan. The reviewed documentation does not establish a single requests-per-minute value for all accounts. Do not size a bulk job around a number copied from another account or an old example.

Inspect response headers such as x-ratelimit-limit, x-ratelimit-remaining, and the reset headers URLbox returns. Log these with each request’s status and request ID. The exact reset header name and representation should be read from the response rather than assumed in client code.

  • x-ratelimit-limit: the limit reported for the current window.
  • x-ratelimit-remaining: the remaining allowance reported for that window.
  • Reset header(s): when the current rate-limit window resets.
  • Retry-After: when present on a rate-limited response, the number of seconds to wait before retrying.

Verify the account-specific allowance in the URLbox dashboard or with URLbox support if headers are absent or unclear. The API reference describes JSON errors with a human-readable message and sometimes a code and request ID, which are useful in operational logs.

2. Handle an API-level 429 correctly

URLbox maps HTTP 429 to “Too many requests — Rate limit was reached.” Its rate-limit guidance says a rate-limited response carries Retry-After in seconds. Stop immediate retries, wait at least that interval, then resume with controlled concurrency while checking the remaining and reset headers.

  1. Record the status, response body, Retry-After, rate-limit headers, and request ID.
  2. Pause the affected account’s queue for the indicated duration. If multiple workers share the account, coordinate the pause across them.
  3. After the wait, resume gradually. Avoid releasing every queued request at once.
  4. Continue watching the headers and adjust the request rate if the remaining allowance falls faster than expected.

If a 429 response has no usable Retry-After, use the reset information if available. Otherwise apply a conservative, bounded exponential backoff with jitter and alert if throttling persists. This fallback is a client-side engineering practice; the documented instruction is to honor Retry-After when supplied.

3. Schedule bulk captures with a queue

Use a durable queue for large URL lists. Give it a per-account rate limiter and a bounded number of in-flight renders. A queue lets the producer add URLs faster than the API can accept them without turning a temporary burst into a storm of 429 responses.

  1. Normalize and validate inputs. Reject malformed URLs and unsupported render options before consuming request capacity.
  2. Choose a small initial concurrency. Increase it only while observed quota headers and render completion times support doing so.
  3. Track each URL independently. Store its status, attempt count, response metadata, and final result so a worker restart does not lose progress.
  4. Pause on throttling. Coordinate workers by account, honor Retry-After, and resume gradually.
  5. Retry only transient failures. Do not repeatedly submit deterministic errors such as invalid options or malformed inputs.

URLbox’s bulk capture guide recommends batch processing and spacing requests for very large sites. That guide is hosted on URLbox’s staging site, so treat it as vendor workflow guidance rather than a contractual quota or throughput guarantee. It does not define a universal batch size. Tune batch size and concurrency for your plan, render duration, retry budget, and completion deadline.

4. Choose synchronous or asynchronous rendering

URLbox documents sync and async render endpoints. Use the synchronous endpoint when the caller needs the generated result in the request-response flow. Use the asynchronous endpoint when a worker can submit a render and collect completion later by polling or webhook. Async can fit long-running or decoupled job systems, but it does not remove the plan’s request quota. Large async submissions still need paced, queued submission.

Workflow Useful when Operational concern
/v1/render/sync The caller needs the render result inline. Set suitable client timeouts and keep the caller from retrying a slow request blindly.
/v1/render/async A worker can submit, then poll or receive a webhook. Persist the render identifier and make polling or webhook handling idempotent.

Use the endpoint paths and authentication format shown in your URLbox account’s current API documentation. The examples below show the queue and retry control flow; insert the account’s documented render parameters and authentication without assuming a universal account configuration.

5. Python example: bounded queue and Retry-After

This runnable pattern uses only Python’s standard library. Set URLBOX_RENDER_URL to the URLbox sync endpoint from your account documentation and URLBOX_AUTHORIZATION to the authorization value required by your account. It submits one URL at a time; a production queue can run a small fixed number of these workers while sharing the same account-level rate limiter.

import os
import time
import random
import requests
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone

RENDER_ENDPOINT = os.environ["URLBOX_RENDER_URL"]
AUTHORIZATION = os.environ["URLBOX_AUTHORIZATION"]
MAX_ATTEMPTS = 5


def retry_after_seconds(response):
    value = response.headers.get("Retry-After")
    if not value:
        return None
    try:
        return max(0.0, float(value))
    except ValueError:
        # HTTP also permits a date form. Support it if returned.
        try:
            when = parsedate_to_datetime(value)
            if when.tzinfo is None:
                when = when.replace(tzinfo=timezone.utc)
            return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
        except (TypeError, ValueError, OverflowError):
            return None


def capture(page_url):
    for attempt in range(MAX_ATTEMPTS):
        response = requests.get(
            RENDER_ENDPOINT,
            params={"url": page_url},  # Add documented render options here.
            headers={"Authorization": AUTHORIZATION},
            timeout=120,
        )
        print({
            "url": page_url,
            "status": response.status_code,
            "request_id": response.headers.get("x-request-id"),
            "limit": response.headers.get("x-ratelimit-limit"),
            "remaining": response.headers.get("x-ratelimit-remaining"),
            "retry_after": response.headers.get("Retry-After"),
        })

        if response.status_code == 429:
            delay = retry_after_seconds(response)
            if delay is None:
                delay = min(60, 2 ** attempt) + random.uniform(0, 1)
            if attempt == MAX_ATTEMPTS - 1:
                response.raise_for_status()
            time.sleep(delay)
            continue

        # Avoid retrying deterministic client errors. Add narrowly scoped retries
        # for transient statuses only if appropriate for your workflow.
        response.raise_for_status()
        return response.content

    raise RuntimeError("Retry budget exhausted")


if __name__ == "__main__":
    urls = ["https://example.com/", "https://www.iana.org/"]
    for index, page_url in enumerate(urls, start=1):
        content = capture(page_url)
        with open(f"capture-{index}.bin", "wb") as output:
            output.write(content)

Install the dependency with python -m pip install requests. Adapt the output extension and content handling to the render format requested. If you add parallel workers, they must share an account-wide limiter; independent per-thread counters can collectively exceed the same quota.

6. Node.js example: paced sequential captures

This example uses Node.js with the built-in fetch API. Set the endpoint and authorization using environment variables as above. The response body is written as a binary file; use your account’s documented parameters and output format.

import { writeFile } from "node:fs/promises";

const endpoint = process.env.URLBOX_RENDER_URL;
const authorization = process.env.URLBOX_AUTHORIZATION;
if (!endpoint || !authorization) {
  throw new Error("Set URLBOX_RENDER_URL and URLBOX_AUTHORIZATION");
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function capture(pageUrl, index) {
  const maxAttempts = 5;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    const requestUrl = new URL(endpoint);
    requestUrl.searchParams.set("url", pageUrl);
    const response = await fetch(requestUrl, {
      headers: { Authorization: authorization },
      signal: AbortSignal.timeout(120_000),
    });

    console.log({
      url: pageUrl,
      status: response.status,
      requestId: response.headers.get("x-request-id"),
      limit: response.headers.get("x-ratelimit-limit"),
      remaining: response.headers.get("x-ratelimit-remaining"),
      retryAfter: response.headers.get("retry-after"),
    });

    if (response.status === 429) {
      const header = response.headers.get("retry-after");
      const seconds = header === null ? null : Number(header);
      const delayMs = Number.isFinite(seconds)
        ? Math.max(0, seconds * 1000)
        : Math.min(60_000, 2 ** attempt * 1000) + Math.random() * 1000;
      if (attempt === maxAttempts - 1) {
        throw new Error(`429 after ${maxAttempts} attempts for ${pageUrl}`);
      }
      await sleep(delayMs);
      continue;
    }

    if (!response.ok) {
      throw new Error(`Render failed: HTTP ${response.status} for ${pageUrl}`);
    }
    await writeFile(`capture-${index}.bin`, Buffer.from(await response.arrayBuffer()));
    return;
  }
  throw new Error(`Retry budget exhausted for ${pageUrl}`);
}

const urls = ["https://example.com/", "https://www.iana.org/"];
for (let i = 0; i < urls.length; i++) {
  await capture(urls[i], i + 1);
}

The sample handles numeric Retry-After values, which is the seconds format described by URLbox’s rate-limit documentation. If your HTTP stack receives an HTTP-date form, parse it as a date too. For a production worker, add a shared queue, persistent result state, bounded concurrency, and coordinated pauses across processes.

7. cURL: inspect a single response

Use -i to display response headers while diagnosing an individual request. Supply the authentication and render options exactly as specified in your URLbox account documentation.

curl -i -G "$URLBOX_RENDER_URL" \
  -H "Authorization: $URLBOX_AUTHORIZATION" \
  --data-urlencode "url=https://example.com/" \
  --output capture.bin

For an automated bulk workload, cURL alone is not a queue or shared rate limiter. Wrap requests in a job runner that captures headers and response bodies, honors Retry-After, and limits concurrent requests. Avoid printing credentials into logs or shell history.

8. URLbox server-managed retry options

URLbox’s render options include retry_on for selected conditions, including target-page statuses such as 429 and 503, plus render failures such as timeouts or crashes and small output size. The options reference documents max_retries and max_attempts, with allowed values from 0 through 5 and documented defaults of 2 and 3 respectively. It also documents retry_delay_ms from 100 through 60,000; each exponential wait is capped at 30 seconds. These retry controls require Ultra or above.

Use URLbox-side retries for selected render conditions, and client-side handling for throttling of your API submissions. A URLbox render retry that reacts to a target page response does not replace a client honoring Retry-After on an API-level 429. Keep maximum attempts bounded so a difficult URL does not consume an unplanned portion of the job’s deadline.

Consult the current render options reference for exact parameter syntax and plan availability before enabling these settings. Avoid retrying every failure indiscriminately: invalid options and malformed requests will not be fixed by repetition.

9. Distinguish API throttling from target-site blocking

The same status code can describe different request layers:

What happened Where the status came from What to do
API-level 429 Your request to URLbox was rate limited. Inspect the API error and rate-limit headers; honor Retry-After and slow the account queue.
Target-site 429 or 403 The website URLbox rendered returned a block or denial response. Inspect the render result and configure render-level checks/retries where appropriate.
Challenge page with HTTP 200 The target returned a page that looks successful at HTTP level but contains a CAPTCHA or challenge. Validate output size or look for known challenge selectors; a status-only rule may miss it.

URLbox’s blocked-render guide notes that some challenge pages respond with HTTP 200, so status-based options never trigger. It documents size checks such as min_size_bytes with small_size retries, and selector checks for known challenge elements. These checks address render content; they do not change your API request quota.

10. Performance, reliability, and cost planning

  • Performance: Throughput depends on the account’s actual minute allowance and how long renders take. Async submission can free a caller from waiting for each result, but submission still needs pacing. Measure end-to-end completion time, queue age, and render duration.
  • Reliability: Persist queue state and use idempotent result handling. Record status, request ID, rate headers, and retry count. Use bounded retries and route exhausted jobs to an error queue for inspection.
  • Quota: Track API requests against the documented plan limit. Leave capacity for retries and other jobs sharing the account. Do not infer a quota from successful bursts.
  • Cost: Check your current URLbox plan and billing terms for the costs and limits that apply to your account. The reviewed sources establish plan-dependent rate limits but do not provide a universal price, render cost, or throughput guarantee.

11. Troubleshooting

Symptom Likely cause Fix
API response is 429 Request rate exceeded the account’s current per-minute allowance. Pause for Retry-After, coordinate the pause across workers, then resume at lower concurrency.
429 repeats after waiting Other workers are consuming the shared quota, or the client resumed with a burst. Use one account-wide limiter and resume gradually while observing remaining/reset headers.
No fixed rate-limit number is visible The quota is plan-specific or the response headers were not captured. Inspect raw headers and verify the current account limit in the dashboard or with URLbox support.
Render result shows target page 403 or 429 The destination site, rather than the URLbox API, blocked or limited the renderer. Inspect the render-level result; use target-status retry or failure rules only when a retry can help.
Render looks like a CAPTCHA despite HTTP 200 The site served a soft-block or challenge page with a successful HTTP status. Validate minimum output size or challenge selectors; status-only retries will not detect it.
Requests time out in the client The render duration exceeds the client timeout, or the caller is waiting inline for a slow render. Set an appropriate timeout; consider async submission with polling/webhook handling for decoupled work.
Retry attempts do not occur Retry options may be unavailable on the account plan, or the failure condition is not configured. Check Ultra-or-above availability and the current options reference; handle API-level 429s in the client.
Many duplicate captures appear A caller retried after losing the response without recording submission state. Persist job state and reconcile request IDs/results before resubmitting uncertain jobs.

12. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server by Yorker Media. Its one-call API can return a screenshot or PDF, so you do not need to run a browser fleet for straightforward captures. See the ScreenshotNeo API documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are never billed. Responses say which page verdict occurred and whether it was billed.
  • An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
  • 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000 screenshots.

Sign up free for 1,000 screenshots a month, with no card required.

13. FAQ

What is URLbox’s screenshot API rate limit?

It depends on the plan. Check the account’s response headers and verify the current limit for that account; the reviewed documentation does not give one universal quota.

Can async rendering bypass the rate limit?

No. Async changes result delivery and lets a worker poll or consume a webhook; it does not establish a separate unlimited submission allowance.

Should I retry every 429?

Retry an API-level 429 only after waiting for the supplied Retry-After interval, with a bounded attempt policy. A target website’s 429 is a separate render condition.

Can a successful HTTP status still produce a blocked screenshot?

Yes. A target can return a challenge page with HTTP 200. Check output size or challenge selectors as well as status codes.

What batch size should I use?

There is no universal batch size in the reviewed URLbox guidance. Start conservatively and tune from your account quota, render time, and completion deadline.