# Captcha Solver Concurrency, 429s and Retry-After

> Run many Cloudflare Turnstile tasks at once: how ZeroCaptcha paces calls, what 429, ERROR_RATE_LIMIT and ERROR_NO_SLOT_AVAILABLE mean, and how to back off.

- Source: https://zerocaptcha.io/guides/rate-limits-and-concurrency
- Published: 2026-09-30
- Updated: 2026-10-01
- Author: ZeroCaptcha Engineering

A single Turnstile task is easy. A thousand at once raises new questions: how many can run in
parallel, what the API does when you send too many, and how your code should slow down without
losing work. This guide explains how ZeroCaptcha paces calls, the signals it sends, and a pattern
for high-volume pipelines.

## What is limited, and what is not

- **createTask has no call budget.** What bounds it is your balance, since each task holds its
  price, and your account's share of the queue. There is no rate limit on paid tasks as a pricing
  tier, and no plan to upgrade.
- **The queue is shared fairly.** Workers take tasks from the account served least recently, so one
  account's burst does not starve another, and each account may have 50 tasks queued or running at
  once by default. When your share is full, `createTask` answers `ERROR_NO_SLOT_AVAILABLE`
  (`queue_full` in REST) until one of your tasks ends.
- **Reads have a budget.** `getTaskResult` and `getBalance` share a budget per key and account.
  Polling far faster than every second or two is answered with `ERROR_RATE_LIMIT`
  (`rate_limited` in REST) and a `Retry-After` header.

## The "try again later" signals

| Signal | Where | What to do |
| --- | --- | --- |
| HTTP 429 | Any format | Wait for `Retry-After`, then send the same request |
| HTTP 5xx | Any format | Back off and retry; keep the same `Idempotency-Key` |
| `ERROR_RATE_LIMIT` | Compatible | Poll less often; wait for `Retry-After` |
| `ERROR_NO_SLOT_AVAILABLE` | Compatible | Wait a few seconds, then retry |
| `ERROR_SERVICE_UNAVAILABLE` | Compatible | Retry with backoff |
| `MAX_USER_TURN`, `ERROR: 1005` | 2Captcha | Wait the seconds in `Retry-After` |

`Retry-After` is either a number of seconds or an HTTP date. Honor it when it is present. When it
is not, wait one second and double the pause on each retry, up to around 16 seconds.

## A bounded worker pool

The simplest way to stay inside limits is to cap how many tasks you have in flight, instead of
creating every task at once:

```js
const API = process.env.ZEROCAPTCHA_API;
const KEY = process.env.ZEROCAPTCHA_KEY;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function call(method, body, headers = {}) {
  for (let pause = 1000; ; pause = Math.min(pause * 2, 16000)) {
    const reply = await fetch(`${API}/${method}`, {
      method: "POST",
      headers: { "Content-Type": "application/json", ...headers },
      body: JSON.stringify({ clientKey: KEY, ...body }),
    });
    const retryAfter = Number(reply.headers.get("retry-after"));
    if (reply.status === 429 || reply.status >= 500) {
      await sleep(retryAfter > 0 ? retryAfter * 1000 : pause);
      continue;
    }
    const json = await reply.json();
    const busy = ["ERROR_RATE_LIMIT", "ERROR_NO_SLOT_AVAILABLE", "ERROR_SERVICE_UNAVAILABLE"];
    if (json.errorId === 1 && busy.includes(json.errorCode)) {
      await sleep(retryAfter > 0 ? retryAfter * 1000 : pause);
      continue;
    }
    return json;
  }
}

async function solve(task) {
  const created = await call("createTask", { task }, { "Idempotency-Key": crypto.randomUUID() });
  if (created.errorId) throw new Error(created.errorCode);
  for (;;) {
    await sleep(2000);
    const result = await call("getTaskResult", { taskId: created.taskId });
    if (result.errorId) throw new Error(result.errorCode);
    if (result.status === "ready") return result.solution.token;
  }
}

// At most 50 tasks in flight.
async function runAll(tasks, limit = 50) {
  const results = [];
  let next = 0;
  await Promise.all(Array.from({ length: limit }, async () => {
    while (next < tasks.length) {
      const index = next++;
      results[index] = await solve(tasks[index]).catch((error) => error);
    }
  }));
  return results;
}
```

The idempotency key is made once per task and reused on every retry of that `createTask`, so a
retry never makes a second task: see [Idempotency keys](https://zerocaptcha.io/guides/idempotency-keys-for-captcha-tasks).
Production code should also stop waiting after a deadline, as the [quickstart](https://zerocaptcha.io/docs/quickstart)
samples do.

## Poll less, or not at all

Polling is most of your call volume. Two ways to reduce it:

- **Poll every two seconds**, not every hundred milliseconds. A task finishes when it is solved;
  asking more often does not speed it up.
- **Use callbacks.** Name a `callbackUrl` and the result is posted to you when the task ends. Your
  read budget is then almost untouched. See [Captcha solver callbacks](https://zerocaptcha.io/guides/captcha-solver-callbacks).

## Size the pool from real numbers

The [status page](https://zerocaptcha.io/status) shows the platform's median solve time over the last 24 hours. With a
median of a few seconds, a pool of 50 keeps many tasks moving per minute. Keep the pool at or under
your account's share, 50 by default: a larger pool only meets `ERROR_NO_SLOT_AVAILABLE` more
often. Your balance is the other
bound: each task in flight holds its price, so make sure the available balance covers a full pool.

## Tokens still expire

A large pool that solves faster than your forms consume tokens creates expired tokens, which are
charged and useless. Solve at the pace you submit. [Cloudflare Turnstile token expiry](https://zerocaptcha.io/guides/cloudflare-turnstile-token-expiry)
has the details, and the [Cloudflare Turnstile solver](https://zerocaptcha.io/cloudflare-turnstile-solver) page shows the flow in other
languages.

## Questions

### Is there a limit on how many tasks I can create?

Your balance and your account's share of the queue bound createTask; there is no call budget on it and no paid tier for volume. Reads such as getTaskResult share a budget per key and account.

### How should I wait after a 429?

As long as the Retry-After header says, in seconds or until an HTTP date. Without one, wait a second and double the pause on each retry, up to about 16 seconds.

### Does polling faster make tasks finish sooner?

No. A task finishes when it is solved, however often you ask. Poll every one or two seconds, or use a callback.
