# Cloudflare Turnstile Solve Time Benchmark: How We Measure

> How we measure Cloudflare Turnstile solve time, success rate and availability from the public status feed, with no invented numbers, and how to benchmark yours.

- Source: https://zerocaptcha.io/blog/cloudflare-turnstile-solve-time-benchmark
- Published: 2026-09-30
- Updated: 2026-10-01
- Author: ZeroCaptcha Engineering

We publish solve-time figures for Cloudflare Turnstile only from the API's public status feed,
never from a lab run or a guess. The feed reports three numbers over the last 24 hours: the share
of finished tasks that returned a token, the median time from `createTask` to the token, and the
share of minutes in which the API was answering. **As of 30 September 2026, ZeroCaptcha has not
launched, so the feed has no production figures yet, and this article reports none.** It
publishes the method now, so the figures that follow can be checked against it, and it shows how
to benchmark any solver on your own pages.

## The three figures

The feed is `GET /v1/status` on the API host. It needs no key, and every caller gets the same
reply, which caches may keep for a minute. The [status page](https://zerocaptcha.io/status) shows the same figures,
read live when the page loads.

| Figure | What it measures | When it is empty |
| --- | --- | --- |
| `taskSuccessRate` | Of the tasks that finished in the last 24 hours, the share that returned a token | Fewer than 20 tasks finished |
| `medianSolveSeconds` | The median time from creating a task to its token, over the tasks that succeeded | Fewer than 20 tasks succeeded |
| `apiAvailability` | The share of the window's whole minutes in which the API was serving | No whole minute yet |

A reply looks like this (these values are the API contract's example, not a measurement):

```json
{
  "generatedAt": "2026-10-01T12:00:00Z",
  "windowHours": 24,
  "taskSuccessRate": 0.9612,
  "medianSolveSeconds": 3.1,
  "apiAvailability": 0.9993
}
```

## Rules we hold ourselves to

1. **Only the feed.** Every number in a benchmark post is a figure the feed returned, quoted with
   its `generatedAt` time. No numbers from test targets, no "up to" figures, no best hour.
2. **Too little data is said plainly.** The feed returns `null` for a figure drawn from fewer than
   20 tasks, and we print "not enough data yet" rather than a number.
3. **Dated snapshots.** Each post that quotes the feed gives the date and time the figures were
   read. A figure without a date is not a benchmark.
4. **Failures count.** The success rate's denominator is every task that finished, including the
   ones that failed or timed out. Tasks still running are left out until they finish.
5. **The median, not the mean.** A handful of slow solves drag an average far above what most
   tasks take. The median says what a typical task takes.

## What the feed cannot tell you

The feed measures the service as a whole, across every customer's pages. It cannot tell you:

- **Whether your target accepts the tokens.** A token is only useful if the site's own siteverify
  call accepts it, and that depends on the site's settings: action, hostname, a proxy's region.
- **Your end-to-end time.** Your network, your polling interval and your queue add to the median.
  Polling every 10 seconds instead of 2 can add several seconds on average.
- **How another provider compares.** Providers measure differently, so compare them on your own
  pages, at the same time, with the same code.

## Benchmark a solver on your own pages

A fair comparison sends the same pages to each provider, at the same time of day, with the same
polling interval, and records whether the target accepted the token. This script records the two
numbers that matter for one provider, using the createTask format most providers share:

```python
import os
import statistics
import time
import uuid

import requests

API = os.environ["CAPTCHA_API"]  # the provider's base URL
KEY = os.environ["CAPTCHA_KEY"]
PAGE, SITEKEY = "https://shop.example.com/login", "0x4AAAAAAAB1cD2eF3gH4iJ5"
# Your widget's data-action and data-cdata (or turnstile.render()'s action and cData), if it sets
# them: without them, a site that checks them refuses the token, and the run measures nothing.
ACTION, CDATA = "login", "session-7f3a9c2e"


def one_task() -> tuple[bool, float]:
    started = time.monotonic()
    task = {
        "type": "TurnstileTaskProxyless",
        "websiteURL": PAGE,
        "websiteKey": SITEKEY,
        "metadata": {"action": ACTION, "cdata": CDATA},
    }
    # One Idempotency-Key per task, for providers that take one: a retried create returns the same task.
    created = requests.post(f"{API}/createTask", json={"clientKey": KEY, "task": task},
                            headers={"Idempotency-Key": str(uuid.uuid4())}, timeout=15).json()
    if created["errorId"]:
        return False, time.monotonic() - started
    while time.monotonic() - started < 180:
        time.sleep(2)
        result = requests.post(
            f"{API}/getTaskResult", json={"clientKey": KEY, "taskId": created["taskId"]}, timeout=15
        ).json()
        if result["errorId"]:
            return False, time.monotonic() - started
        if result["status"] == "ready":
            # Submit result["solution"]["token"] to your own form here and record
            # whether your server's siteverify call accepted it.
            return True, time.monotonic() - started
    return False, time.monotonic() - started


runs = [one_task() for _ in range(50)]
times = [seconds for ok, seconds in runs if ok]
print(f"success: {len(times)}/{len(runs)}")
if len(times) >= 20:
    print(f"median solve time: {statistics.median(times):.1f} s")
else:
    print("median solve time: not enough successful tasks")
```

It runs the tasks one after another, which is fine for a sample of 50. Run it against each
provider in the same hour, and add the acceptance check on your own server: count a run as a
success only when siteverify answers `"success": true`. On your own test page, the
[testing sitekeys](https://zerocaptcha.io/guides/test-cloudflare-turnstile-in-ci) do not exercise a real solve, so use the real
widget.

## Reading other providers' numbers

When a provider quotes a solve time or success rate, look for four things: the window it covers,
whether failures are in the denominator, whether it is a median or a mean, and the date. A number
missing any of them cannot be compared with another. The [best Cloudflare Turnstile solvers
roundup](https://zerocaptcha.io/compare/best-cloudflare-turnstile-solvers) sticks to sourced, dated facts for the same reason.

## When figures will appear here

Once the feed has figures, this post will quote a dated snapshot each month, taken from the feed
at the time stated, and its updated date will change with each one. Until then, the live numbers,
or "not enough data yet", are on the [status page](https://zerocaptcha.io/status).

## Sources

- ZeroCaptcha API contract, `GET /v1/status` and the `StatusReport` schema:
  [API reference](https://zerocaptcha.io/docs/reference/api) (checked 1 October 2026).
- [Cloudflare Turnstile: server-side validation](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/),
  for what a site checks when it accepts a token (checked 1 October 2026).

## Questions

### How fast does ZeroCaptcha solve Cloudflare Turnstile?

The live answer is on the status page: the median time from createTask to token over the last 24 hours. On 30 September 2026 the service had not launched, so there was no production figure yet, and this article does not invent one.

### What is a good success rate for a Cloudflare Turnstile solver?

Measure the share of tokens your target site accepts, not only the share of tasks that return a token. A token the site rejects is worth nothing, whoever produced it.

### Why is the median used instead of the average solve time?

A few slow tasks pull an average up a long way. The median says what a typical task takes: half of successful tasks were at least that fast.
