At the Turnstile list prices four large solving services publish (checked 2026-09-29), one million Cloudflare Turnstile tokens a month costs between $1,200 and $2,000. On CaptchaAI's thread plans the same million fits an estimated 8 to 24 threads, STANDARD ($30/month, 15 threads) or ADVANCE ($90/month, 50 threads) depending on how bursty your traffic is. Below is the formula behind that estimate, run for 100K, 1M and 10M solves, plus the two things that break the math: tokens that expire unused, and Cloudflare Challenge pages, which need a proxy and carry a bandwidth bill of their own.
Why a Turnstile thread goes further than a reCAPTCHA v2 thread
CaptchaAI bills per concurrent thread, with unlimited solves per thread for the month. A thread is one CAPTCHA in flight; when that solve finishes, the thread takes the next one. So a thread's monthly capacity is set by time: seconds in the month divided by seconds per solve.
CaptchaAI's published ceiling for Cloudflare Turnstile is under 10 seconds per solve, against under 60 seconds for reCAPTCHA v2. Divide a 30-day month (2,592,000 seconds) by each ceiling:
| CAPTCHA type | Published ceiling | Theoretical max per thread per month |
|---|---|---|
| Cloudflare Turnstile | <10 s | ~259,000 |
| Cloudflare Challenge | <15 s | ~173,000 |
| reCAPTCHA v2 | <60 s | ~43,200 |
In theory, one Turnstile thread clears six times the volume of a reCAPTCHA v2 thread. Two caveats stop that number from being a plan recommendation on its own. The figures are ceilings, not averages, so an estimate built on them errs toward buying more threads. And the maximum assumes the thread never sits idle, which only happens if jobs arrive in a perfectly even stream, so the sizing below plans for peaks. The same reasoning for every CAPTCHA type is in how the unlimited-solves model works.
The sizing formula
CaptchaAI's published plan-sizing tables have no Turnstile row yet, so every figure in this section and the next is an estimate from this formula, not a vendor recommendation. It is Little's law applied to threads:
busy_threads_at_peak = V * P * T / 2,592,000
threads_to_buy >= busy_threads_at_peak / U
- V is Turnstile solves per month.
- P is your peak-to-average ratio: solves in your busiest hour divided by average hourly solves. A 24/7 crawler that spreads work evenly sits near 1. A job that only runs in an 8-hour window is about 3.
- T is how long a thread stays busy per solve. Use 10 seconds, the published ceiling, until you have measured your own; the FAQ shows how, and why request timings are the wrong tool.
- U is the utilization you accept in the busiest hour. At 0.5, half the threads sit idle on average during that hour, which absorbs the busier minutes inside it, retries and slow solves. That is a wider margin than the ×1.3 to ×1.5 the thread count optimizer applies, because the optimizer starts from your busiest minute, while P here is measured per hour and hides the spikes within it.
This script runs the formula for three volumes, picks the smallest plan with enough threads, and prints each vendor's per-solve bill beside it. Pass your own P and U as arguments.
"""Estimate CaptchaAI threads for a Cloudflare Turnstile workload.
This is an estimate: it uses the published <10 s Turnstile ceiling as the
time a thread stays busy, not a measured average from your own traffic.
Usage: python turnstile_sizing.py [peak_ratio] [target_utilization]
"""
import math
import sys
SECONDS_PER_MONTH = 30 * 24 * 3600 # 2,592,000
TURNSTILE_CEILING_S = 10
# (plan, monthly list price in USD, threads) from captchaai.com/pricing
PLANS = [
("BASIC", 15, 5), ("STANDARD", 30, 15), ("ADVANCE", 90, 50),
("PREMIUM", 170, 100), ("CORPORATE", 240, 150), ("ENTERPRISE", 300, 200),
("VIP-1", 1500, 1000), ("VIP-2", 4500, 3000), ("VIP-3", 7500, 5000),
]
# Turnstile list price per 1,000 solves, checked 2026-09-29
PER_1K = {"2Captcha": 1.45, "CapSolver": 1.20, "Anti-Captcha": 2.00, "CapMonster Cloud": 1.30}
def size(monthly_solves, peak_ratio, utilization, solve_s=TURNSTILE_CEILING_S):
busy_at_peak = monthly_solves * peak_ratio * solve_s / SECONDS_PER_MONTH
needed = max(1, math.ceil(busy_at_peak / utilization))
for plan, price, threads in PLANS:
if threads >= needed:
return busy_at_peak, needed, plan, price, threads
raise ValueError(f"{needed} threads is beyond the largest listed plan")
def main():
peak = float(sys.argv[1]) if len(sys.argv) > 1 else 1.0
util = float(sys.argv[2]) if len(sys.argv) > 2 else 0.5
if peak < 1 or not 0 < util <= 1:
sys.exit("peak_ratio must be >= 1 and target_utilization in (0, 1]")
print(f"peak ratio {peak:g}, target utilization {util:.0%} (estimate)")
for volume in (100_000, 1_000_000, 10_000_000):
busy, needed, plan, price, threads = size(volume, peak, util)
print(f"{volume:>10,}/month: {busy:5.1f} busy at peak, need {needed:>3} "
f"-> {plan} ${price:,}/month ({threads:,} threads)")
print(" per-solve: " + ", ".join(
f"{name} ${volume / 1000 * rate:,.0f}" for name, rate in PER_1K.items()))
if __name__ == "__main__":
main()
Output of python turnstile_sizing.py 3 (a workload concentrated in an 8-hour window):
peak ratio 3, target utilization 50% (estimate)
100,000/month: 1.2 busy at peak, need 3 -> BASIC $15/month (5 threads)
per-solve: 2Captcha $145, CapSolver $120, Anti-Captcha $200, CapMonster Cloud $130
1,000,000/month: 11.6 busy at peak, need 24 -> ADVANCE $90/month (50 threads)
per-solve: 2Captcha $1,450, CapSolver $1,200, Anti-Captcha $2,000, CapMonster Cloud $1,300
10,000,000/month: 115.7 busy at peak, need 232 -> VIP-1 $1,500/month (1,000 threads)
per-solve: 2Captcha $14,500, CapSolver $12,000, Anti-Captcha $20,000, CapMonster Cloud $13,000
Worked estimates for 100K, 1M and 10M Turnstile solves
With U = 0.5, the formula gives these plans for a steady workload and a bursty one:
| Monthly Turnstile solves | Steady (P = 1): threads needed | Plan | Bursty (P = 3): threads needed | Plan |
|---|---|---|---|---|
| 100,000 | 1 | BASIC ($15/month, 5 threads) | 3 | BASIC ($15/month, 5 threads) |
| 1,000,000 | 8 | STANDARD ($30/month, 15 threads) | 24 | ADVANCE ($90/month, 50 threads) |
| 10,000,000 | 78 | PREMIUM ($170/month, 100 threads) | 232 | VIP-1 ($1,500/month, 1,000 threads) |
The peak ratio moves the bill more than the volume does. Tripling P triples the thread count. At 10M bursty solves, 232 threads lands just past ENTERPRISE ($300/month, 200 threads), which would run at about 58% of its threads at peak (115.7 busy of 200). That is defensible if your peaks are predictable and a queue sits in front of the solver; otherwise take VIP-1. Measure P for a week before you commit, because it is the input most likely to be wrong.
Tight fits fail on bad days. At 1M bursty solves, STANDARD's 15 threads would run at 77% at peak (11.6 of 15). A retry storm or a slow minute then has nowhere to go, which is what the 0.5 target absorbs.
Turnstile lands lower than reCAPTCHA v2 at the same volume. CaptchaAI's canonical reCAPTCHA v2 sizing works from average volume, like the steady column, and puts 1M solves a month on ADVANCE ($90/month) and 10M on ENTERPRISE ($300/month). The steady Turnstile estimates sit one or two tiers lower. The reCAPTCHA v2 cost analysis covers that side, and the thread count optimizer linked above handles workloads that mix Turnstile with other types.
Per-solve Turnstile rates, checked 2026-09-29
These are the list prices for Cloudflare Turnstile on each vendor's own pricing page. Vendors change prices and some sell volume packages, so re-check before you rely on them.
| Vendor | Turnstile price per 1,000 | Source |
|---|---|---|
| 2Captcha | $1.45 | 2captcha.com/pricing |
| CapSolver | $1.20 | CapSolver pricing docs |
| Anti-Captcha | $2.00 | anti-captcha.com price table |
| CapMonster Cloud | $1.30 | capmonster.cloud/en/prices |
Monthly cost at those rates, next to the CaptchaAI estimates from the previous section:
| Monthly solves | 2Captcha | CapSolver | Anti-Captcha | CapMonster Cloud | CaptchaAI steady (estimate) | CaptchaAI bursty (estimate) |
|---|---|---|---|---|---|---|
| 100,000 | $145 | $120 | $200 | $130 | BASIC: $15 | BASIC: $15 |
| 1,000,000 | $1,450 | $1,200 | $2,000 | $1,300 | STANDARD: $30 | ADVANCE: $90 |
| 10,000,000 | $14,500 | $12,000 | $20,000 | $13,000 | PREMIUM: $170 | ENTERPRISE: $300 or VIP-1: $1,500 |
The crossover sits low. BASIC ($15/month) equals the per-solve bill at about 7,500 Turnstile solves a month at Anti-Captcha's rate, 10,300 at 2Captcha's, 11,500 at CapMonster Cloud's and 12,500 at CapSolver's. Below that, a per-solve account is cheaper or level; above it, the gap widens with every token, because extra volume on a thread you already pay for costs nothing more. Thread-based vs per-solve pricing applies the same break-even logic to other CAPTCHA types. Prepaid periods cut the thread price further, 5% off semi-annual and 10% off annual at the time of writing, per captchaai.com/pricing.
Wasted tokens cost money on both models
Turnstile tokens are short-lived and single-use. Cloudflare's server-side validation docs state that a token is valid for 300 seconds after it is generated and can be validated only once; an expired or replayed token comes back from Siteverify with timeout-or-duplicate. On a per-solve account, every token that expires in a queue is a billed solve. On a thread plan it is thread time you got nothing from, which eats the headroom the formula reserved.
- Solve just in time. Fill in the form first, then request the token, then submit. Never pre-solve a pool of tokens to ride out a peak, because they expire before the peak ends; queue the work instead. Turnstile token expiration timing covers the race conditions in multi-step forms.
- Budget less than 300 seconds. The clock starts when the token is generated on the solver's side, not when your code receives it. The examples below refuse to post a token held for more than 240 seconds.
- Match what Siteverify reports. A Siteverify response includes the
hostnameandactionthe token was issued for, and the site's backend can check both. Send the exactpageurl, and sendactionwhenever the widget setsdata-action. - Do not spend solves on test keys. On your own staging site, use Cloudflare's dummy sitekeys:
1x00000000000000000000AAalways passes and yields the tokenXXXX.DUMMY.TOKEN.XXXX. Pair it with the always-pass test secret1x0000000000000000000000000000000AA, because production secrets reject the dummy token. QA runs then need no solver and no threads.
Calling method=turnstile from Python and Node.js
Use these examples on forms you own or are authorized to automate. The request is small:
- Read the widget's
data-sitekey, or thesitekeypassed toturnstile.render(), plus the exact page URL. Production sitekeys typically start with0x4AAAAAAA; one starting1x,2xor3xis a Cloudflare test key. Sitekey extraction covers widgets rendered from JavaScript. - Submit to
https://ocr.captchaai.com/in.phpwithkey,method=turnstile,sitekey,pageurlandjson=1. Optional fields areaction(the widget'sdata-action) andproxywithproxytype. CaptchaAI's proxy guide advises a proxy only when the site enforces IP matching, and proxy use stays off until support enables it on your account. - A good submit returns
{"status":1,"request":"<task id>"}. Wait 10 to 15 seconds, then pollhttps://ocr.captchaai.com/res.phpwithaction=get, the taskidandjson=1every 5 seconds while it answersCAPCHA_NOT_READY. The examples also keep polling throughERROR_INTERNAL_SERVER_ERRORand HTML 5xx pages, which are transient. - The token arrives in
request. Put it in the hidden input the widget adds, which Cloudflare namescf-turnstile-responseunless the site renames it with the widget'sresponse-field-nameoption. Do not default tog-recaptcha-responseout of reCAPTCHA habit; that name is only right when the page loads Turnstile'sapi.jswith?compat=recaptcha(reCAPTCHA compatibility mode).
Note the two action fields: on in.php it is the Turnstile action from the page, on res.php the literal get. And ERROR_ZERO_BALANCE from in.php means no free threads or no active plan, not an empty wallet; both examples back off for 10 seconds and try the submit up to three times, and the FAQ shows how to tell which case you hit.
"""Solve one Cloudflare Turnstile widget with CaptchaAI and post the form.
Requires: pip install requests
Env: CAPTCHAAI_API_KEY, TURNSTILE_SITEKEY, TURNSTILE_PAGEURL, and optionally
TURNSTILE_ACTION (the widget's data-action) and FORM_ACTION_URL.
"""
import json
import os
import sys
import time
import requests
API = "https://ocr.captchaai.com"
TOKEN_BUDGET_S = 240 # Cloudflare accepts a token for 300 s after generation
KEEP_POLLING = ("CAPCHA_NOT_READY", "ERROR_INTERNAL_SERVER_ERROR")
def call(path, params):
text = requests.get(f"{API}/{path}", params=params, timeout=30).text.strip()
if not text.startswith("{"): # some errors come back as plain text
return {"status": 0, "request": text}
return json.loads(text)
def solve_turnstile(key, sitekey, pageurl, action=None, deadline_s=120):
params = {"key": key, "method": "turnstile", "sitekey": sitekey,
"pageurl": pageurl, "json": 1}
if action:
params["action"] = action # Turnstile's data-action, not res.php's action=get
for attempt in range(3):
sub = call("in.php", params)
if sub["request"] != "ERROR_ZERO_BALANCE" or attempt == 2:
break
print("[!] No free threads (or no active plan); retrying in 10 s")
time.sleep(10)
if sub["status"] != 1:
raise RuntimeError(f"submit rejected: {sub['request']}")
task_id = str(sub["request"])
print(f"[+] Task submitted. ID: {task_id}")
time.sleep(10) # the Turnstile guide suggests a first poll after 10-15 s
started = time.monotonic()
while time.monotonic() - started < deadline_s:
res = call("res.php", {"key": key, "action": "get", "id": task_id, "json": 1})
if res["status"] == 1:
return res["request"], time.monotonic()
answer = str(res["request"])
# Not ready, or a transient server error / HTML 5xx page: poll again.
if answer not in KEEP_POLLING and not answer.startswith("<"):
raise RuntimeError(f"solve failed: {answer}")
time.sleep(5)
raise TimeoutError(f"task {task_id} not solved within {deadline_s} s")
def main():
key = os.environ.get("CAPTCHAAI_API_KEY", "")
sitekey = os.environ.get("TURNSTILE_SITEKEY", "")
pageurl = os.environ.get("TURNSTILE_PAGEURL", "")
if len(key) != 32:
sys.exit("Set CAPTCHAAI_API_KEY to your 32-character CaptchaAI key")
if not sitekey or any(c.isspace() for c in sitekey):
sys.exit("Set TURNSTILE_SITEKEY to the widget's data-sitekey value")
if not pageurl.startswith(("https://", "http://")):
sys.exit("Set TURNSTILE_PAGEURL to the exact URL of the page with the widget")
token, received_at = solve_turnstile(key, sitekey, pageurl,
os.environ.get("TURNSTILE_ACTION"))
print(f"[+] Solved! Token: {token[:50]}...")
# Default field name; use the site's response-field-name if it sets one,
# and add the form's other fields here.
form = {"cf-turnstile-response": token}
# Anything slow between solve and post (other steps, a queue) ages the token.
if time.monotonic() - received_at > TOKEN_BUDGET_S:
sys.exit("[x] Token too old to submit; solve again just before posting")
resp = requests.post(os.environ.get("FORM_ACTION_URL", pageurl), data=form, timeout=30)
print(f"[+] Form posted: HTTP {resp.status_code}")
if __name__ == "__main__":
main()
The Node.js version needs Node 18 or later for the built-in fetch and has no dependencies:
// Node.js 18+ (built-in fetch, no dependencies). Same env vars as the Python version.
const API = 'https://ocr.captchaai.com';
const TOKEN_BUDGET_MS = 240_000; // Cloudflare accepts a token for 300 s after generation
const KEEP_POLLING = ['CAPCHA_NOT_READY', 'ERROR_INTERNAL_SERVER_ERROR'];
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function call(path, params) {
const res = await fetch(`${API}/${path}?${new URLSearchParams(params)}`);
const text = (await res.text()).trim();
// Some errors come back as plain text even when json=1 was sent.
return text.startsWith('{') ? JSON.parse(text) : { status: 0, request: text };
}
async function solveTurnstile({ key, sitekey, pageurl, action }, deadlineMs = 120_000) {
const params = { key, method: 'turnstile', sitekey, pageurl, json: 1 };
if (action) params.action = action; // Turnstile's data-action, not res.php's action=get
let sub;
for (let attempt = 0; attempt < 3; attempt++) {
sub = await call('in.php', params);
if (sub.request !== 'ERROR_ZERO_BALANCE' || attempt === 2) break;
console.log('[!] No free threads (or no active plan); retrying in 10 s');
await sleep(10_000);
}
if (sub.status !== 1) throw new Error(`submit rejected: ${sub.request}`);
const taskId = String(sub.request);
console.log(`[+] Task submitted. ID: ${taskId}`);
await sleep(10_000); // the Turnstile guide suggests a first poll after 10-15 s
const started = Date.now();
while (Date.now() - started < deadlineMs) {
const res = await call('res.php', { key, action: 'get', id: taskId, json: 1 });
if (res.status === 1) return { token: res.request, receivedAt: Date.now() };
const answer = String(res.request);
// Not ready, or a transient server error / HTML 5xx page: poll again.
if (!KEEP_POLLING.includes(answer) && !answer.startsWith('<')) {
throw new Error(`solve failed: ${answer}`);
}
await sleep(5_000);
}
throw new Error(`task ${taskId} not solved within ${deadlineMs / 1000} s`);
}
async function main() {
const key = process.env.CAPTCHAAI_API_KEY || '';
const sitekey = process.env.TURNSTILE_SITEKEY || '';
const pageurl = process.env.TURNSTILE_PAGEURL || '';
if (key.length !== 32) throw new Error('Set CAPTCHAAI_API_KEY to your 32-character key');
if (!sitekey || /\s/.test(sitekey)) throw new Error('Set TURNSTILE_SITEKEY to data-sitekey');
if (!/^https?:\/\//.test(pageurl)) throw new Error('Set TURNSTILE_PAGEURL to the page URL');
const action = process.env.TURNSTILE_ACTION;
const { token, receivedAt } = await solveTurnstile({ key, sitekey, pageurl, action });
console.log(`[+] Solved! Token: ${token.slice(0, 50)}...`);
// Default field name (or the site's response-field-name); add the form's other fields.
const form = new URLSearchParams({ 'cf-turnstile-response': token });
if (Date.now() - receivedAt > TOKEN_BUDGET_MS) throw new Error('Token too old; solve again');
const resp = await fetch(process.env.FORM_ACTION_URL || pageurl, { method: 'POST', body: form });
console.log(`[+] Form posted: HTTP ${resp.status}`);
}
main().catch((err) => {
console.error(`[x] ${err.message}`);
process.exit(1);
});
A successful run of either version prints three lines (task ID and token shortened here):
[+] Task submitted. ID: 74965409378
[+] Solved! Token: 0.4uMMZZdSfsVM8...
[+] Form posted: HTTP 200
With the variables unset, both exit before any network call. For browser injection with Selenium, see how to solve Cloudflare Turnstile using the API.
Cloudflare Challenge is a different bill
A Cloudflare Challenge page, the full-page interstitial, is not a Turnstile widget. It runs on the same thread plan, but budget it as its own line.
| Cloudflare Turnstile | Cloudflare Challenge | |
|---|---|---|
| CaptchaAI method | turnstile |
cloudflare_challenge |
| Required fields | sitekey, pageurl |
pageurl, proxy, proxytype |
Answer with json=1 |
token in request |
cf_clearance value in result, plus user_agent |
| Published ceiling | <10 s | <15 s |
| What one solve buys | one form submission | a session on one IP and User-Agent |
| Extra cost | none required | proxy bandwidth for the solve and every request after it |
CaptchaAI's docs state that the cf_clearance cookie is bound to both the IP and the User-Agent that solved it, so every later request must use the same proxy and the returned user_agent. CaptchaAI loads the challenge through your proxy, so the solve consumes proxy bandwidth, and so does every page you fetch afterwards. The formula: challenge solves times bytes per solve, plus session requests times average page weight, at your proxy provider's per-GB rate.
The solve count is usually far lower than the request count. Cloudflare's Challenge Passage gives cf_clearance a default lifetime of 30 minutes, with 15 to 45 recommended to site owners, so one solve covers a session of requests on that IP. At the 30-minute default, one sticky proxy IP crawling around the clock needs about 48 solves a day, roughly 1,440 a month, well under 1% of one thread's ~173,000 ceiling. A proxy pool that rotates the exit IP per request breaks that math, because every new IP needs its own solve. On a thread plan the Challenge solves are the small line and the proxy the large one. Of the four per-solve vendors above, two list a Challenge rate, and both charge what they charge for Turnstile: $1.20 per 1,000 at CapSolver and $1.30 at CapMonster Cloud (which calls it Cloudflare Bot Challenge). 2Captcha's and Anti-Captcha's pages list no separate Challenge row. The cf_clearance cookie guide shows how to carry the cookie, User-Agent and proxy together.
FAQ
Is the 10-second Turnstile figure an average?
No. It is CaptchaAI's published ceiling, so using it as T overstates how long a thread stays busy and the estimates above lean toward more threads. Do not replace it with client-side timings: the examples wait 10 seconds before the first poll and then poll in 5-second steps, so a measured submit-to-token time can never come in under the ceiling. Run Little's law backwards instead. On an account that solves only Turnstile, sample res.php?action=threadsinfo once a minute through a busy hour; average working_threads divided by the solves per second you submitted in that hour is your real T. The same samples give your real U, as working_threads over threads. Set TURNSTILE_CEILING_S in the calculator to that T and re-run it.
Can I stockpile Turnstile tokens ahead of a traffic spike?
No. Tokens expire 300 seconds after generation and validate once, so a stockpile turns into timeout-or-duplicate rejections. Put a job queue in front of the solver and size threads for the rate at which it drains.
How do I tell if ERROR_ZERO_BALANCE means my plan is too small?
Call res.php with action=threadsinfo. If working_threads equals threads, every thread is busy, so move up a plan or spread the load to lower your peak ratio. If threads are free, the plan has expired or is not active, so renew it rather than buying more threads. Do not rely on the error alone to spot an undersized plan: the official Python SDK's source notes that the API may queue tasks silently when every thread is busy, so saturation can surface only as longer CAPCHA_NOT_READY polling. The working_threads samples from the previous answer catch it either way.
Next step
Run the calculator with your own peak ratio, pick the plan it lands on, and set that against last month's per-solve invoice. Plans and current discounts are on captchaai.com/pricing, and the Turnstile solver page is at captchaai.com/products/cloudflare-turnstile.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.