API Concurrency Limit: Definition and How to Size It

An API concurrency limit caps how many requests run at the same moment. How it differs from a rate limit, how to size a worker pool, with tested code.

By Simon Chiu · Updated

An API concurrency limit is the maximum number of requests from one client that a service will process at the same moment; a new request over that number is rejected or queued until one of the running requests finishes.

It is not the same as a rate limit, and confusing the two is the most common cause of bursts of 429 errors in screenshot jobs. This entry is part of the screenshot glossary.

What it means in plain terms

Think of a fixed number of slots. Each request takes a slot when it starts and gives it back when it ends. A rate limit asks "how many did you start this minute?". A concurrency limit asks "how many are open right now?".

For fast APIs, the difference hardly matters. Screenshots are slow: a capture loads a whole web page, waits for it to settle and encodes an image. A render that holds its slot for several seconds means a few slots fill up long before you get near a per-minute limit.

How the numbers relate

The link between the two limits is Little's law: requests in flight = arrival rate × time per request.

flowchart LR
  Q["Your queue<br/>(URLs waiting)"] --> P{"Free slot?"}
  P -->|yes| S1["Slot 1: rendering"]
  P -->|yes| S2["Slot 2: rendering"]
  P -->|yes| S3["Slot 3: rendering"]
  P -->|"no: wait, or 429 if you send anyway"| Q
  S1 --> D[Done, slot freed]
  S2 --> D
  S3 --> D

So the most a pool can finish per minute is slots × 60 / seconds per render. A worked table, for illustration only:

Slots Seconds per render Most renders per minute from slots Binding limit if the rate limit is 60/min
2 4 30 Concurrency
5 4 75 Rate (60)
5 8 37 Concurrency
10 3 200 Rate (60)

Whichever number is lower is your real ceiling. A slow page (long waits, full-page captures) halves the throughput of every slot, so measure your own render times before you size anything.

Concurrency in Puppeteer, Playwright and RenderScreenshot

When you run the browser yourself, the concurrency limit is one you set: how many pages you open at once in one Chrome. Too many, and the browser runs out of memory. Here is a pool that never has more than LIMIT pages open:

import puppeteer from 'puppeteer';

const LIMIT = 3; // pages open at the same moment
const urls = Array.from({ length: 8 }, (_, i) => `https://example.com/?n=${i}`);

const browser = await puppeteer.launch();
let inFlight = 0;
let peak = 0;
try {
  let next = 0;
  const worker = async () => {
    while (next < urls.length) {
      const url = urls[next++];
      peak = Math.max(peak, ++inFlight);
      const page = await browser.newPage();
      try {
        await page.goto(url);
        await page.screenshot({ path: `shot-${new URL(url).searchParams.get('n')}.png` });
      } finally {
        await page.close();
        inFlight--;
      }
    }
  };
  await Promise.all(Array.from({ length: LIMIT }, worker));
  console.log(`captured ${urls.length} pages, peak in flight: ${peak}`);
} finally {
  await browser.close();
}

It printed captured 8 pages, peak in flight: 3. Promise.all(urls.map(capture)) would have opened all 8 at once.

In Playwright, the same pool works with a fresh browser context per job, which keeps cookies and storage apart:

import { chromium } from 'playwright';

const LIMIT = 3;
const queue = Array.from({ length: 8 }, (_, i) => `https://example.com/?n=${i}`);

const browser = await chromium.launch();
try {
  const worker = async () => {
    for (let url = queue.shift(); url; url = queue.shift()) {
      const context = await browser.newContext();
      try {
        const page = await context.newPage();
        await page.goto(url);
        await page.screenshot({ path: `pw-${new URL(url).searchParams.get('n')}.png` });
      } finally {
        await context.close();
      }
    }
  };
  await Promise.all(Array.from({ length: LIMIT }, worker));
} finally {
  await browser.close();
}

With RenderScreenshot the browser capacity is ours to manage, and your plan sets the limits. Two documented mechanisms apply:

  • Single requests. If you start more renders than your plan allows at once, the API answers HTTP 429 with the code concurrent_limit. That page has a worker pool sized from GET /v1/usage.
  • Batches. POST /v1/batch takes the whole list and renders a plan-dependent number of URLs in parallel; the rest wait in a queue. That is not an error, it just takes longer.
curl -X POST https://api.renderscreenshot.com/v1/batch \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"urls": ["https://example.com/?n=1", "https://example.com/?n=2", "https://example.com/?n=3"], "options": {"preset": "og_card"}}'

The per-plan numbers are on the rate limits page, and the batch endpoint documents the request. The CLI has the same idea as a flag: rs batch --concurrency <n>.

Tested on 2026-10-15 on macOS 27 (arm64) with Node 22.18.0, [email protected] (Chrome 148.0.7778.97, run with headless: 'shell' on this machine) and [email protected]. The batch request was not run for this page.

Common mistakes

  • Promise.all over the whole list. Every request starts at once. Use a pool, a queue with a concurrency setting, or the batch endpoint.
  • Counting one process only. The API's limit covers your whole account or key. Two servers with three workers each are six requests in flight.
  • Retrying at once. A retry without a delay takes the slot back immediately and keeps the pool full. Wait a short, random time; a slot frees up when any render ends.
  • Treating every 429 the same. A rate limit clears at the next window; a concurrency limit clears when a request finishes. Read the error code.

For self-hosted pools at scale, read scaling Puppeteer screenshots. To hand the pool to us, get a free API key.

Frequently asked questions

What is the difference between a concurrency limit and a rate limit?
A rate limit counts requests started in a time window, for example 60 per minute. A concurrency limit counts requests that are still running right now. Slow requests use up a concurrency limit long before they reach a rate limit.
What HTTP status does a concurrency limit return?
Usually 429 Too Many Requests, the same as a rate limit. Read the error code in the body to tell them apart. RenderScreenshot uses concurrent_limit for one and too_many_requests for the other.
Will more workers make my screenshot jobs faster?
Only up to the concurrency limit. Past it, the extra workers just collect 429 errors and retries.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.