A thread is not a solve. That single distinction is where most CAPTCHA budgets go wrong, in both directions — a 200-thread plan bought for 20,000 daily solves that would fit on five threads, or five threads bought for a workload whose peak needs fifty, followed by a week of debugging "the API is slow."
Under thread-based pricing you rent concurrent slots, not units of work. Solves per thread are unlimited; what is finite is how many can be in flight at once. So the sizing question is never "how many CAPTCHAs do I solve per month?" It is "how many are unresolved simultaneously at my busiest moment?"
The key takeaway: threads needed = peak arrival rate × the time each solve stays in flight. The second term varies by more than 100× across challenge types, so 10,000 image CAPTCHAs an hour and 10,000 reCAPTCHA v2 challenges an hour are the same volume and completely different plans.
A thread is a slot, not a unit of work
Per-solve pricing meters output. Thread-based pricing meters occupancy: a slot is held from submission until you have the answer, then frees for the next task. CaptchaAI's published model is unlimited solves per thread, no per-CAPTCHA fee, no daily cap and no surcharge by challenge type — so the only lever on your bill is how many slots you need at once.
The consequence: a slow challenge type is expensive and a fast one is nearly free, even though the sticker price is identical. That is the inverse of per-solve pricing, and which model wins depends entirely on your challenge mix and how evenly traffic arrives.
The only formula you need
Concurrency is governed by Little's Law, which in this context reads:
threads = arrival_rate × time_in_flight
where arrival_rate is solves per second at peak, and time_in_flight is how long a task occupies a slot — submit to answer, in seconds. Run it in reverse and you get the throughput ceiling of a single thread:
solves_per_hour_per_thread = 3600 / time_in_flight
CaptchaAI publishes an upper-bound solve time per challenge type. Because those are ceilings rather than averages, dividing into them gives a floor on what one thread delivers:
| Challenge type | Published solve time | At least, per thread per hour |
|---|---|---|
| Image CAPTCHA | <0.5s | 7,200 |
| Grid Image / BLS | <1s | 3,600 |
| reCAPTCHA v3 (and v3 Enterprise) | <4s | 900 |
| Cloudflare Turnstile | <10s | 360 |
| Geetest v3 | <12s | 300 |
| Cloudflare Challenge | <15s | 240 |
| reCAPTCHA v2 Invisible | <30s | 120 |
| reCAPTCHA v2 (and v2 Enterprise) | <60s | 60 |
These are the vendor's published figures, not independent measurements, and the latency column is a stated maximum — so sizing against them is deliberately conservative. That is the right posture for a first plan: under-sizing shows up as a stalled pipeline at 3am, over-sizing shows up as a slightly annoying invoice.
Notice the spread. The same five threads that clear 36,000 image CAPTCHAs an hour clear only 300 reCAPTCHA v2 challenges an hour. Nothing about the plan changed. The workload did.
Why average throughput misleads
Take your monthly solve count, divide by the hours in a month, and you will under-size almost every real workload. Three structural reasons.
Arrivals are bursty. Crawls run on schedules, QA suites run on merges, monitoring fires on the minute. A workload averaging 500 solves an hour that lands 70% of its volume in a three-hour window peaks at roughly six times its average, and the plan has to survive the peak.
Latency is not throughput. Doubling threads does not make an individual reCAPTCHA v2 return faster — the published <60s is a property of the challenge, not of your plan. Threads buy width, never speed.
Your own concurrency is a second ceiling. You cannot have more challenges in flight than you have workers able to block on one. Twenty parallel browser sessions hold at most twenty open tasks, whatever the arithmetic says.
A worked example
A QA team runs a regression suite against their own staging environment: 12,000 reCAPTCHA v2 challenges a day, 70% of them inside a three-hour nightly window.
- On the average:
12,000 / 24 = 500/hour = 0.139/s× 60s = 8.3 threads. - On the peak:
8,400 / 3 = 2,800/hour = 0.778/s× 60s = 46.7 threads.
The average answer is off by more than 5×. On the published tiers the peak answer lands inside ADVANCE (50 threads, $90/month), while the average answer points at BASIC (5 threads, $15/month) — which cannot even carry the daily average for this challenge type.
Now apply the second ceiling. If the suite runs only twenty parallel browser sessions, twenty threads is a hard cap on what it can consume. The 47 does not disappear; it reappears as elapsed time. Twenty threads at 60s each clear 1,200 solves an hour, so 8,400 challenges take seven hours rather than three. That is a fine trade to make on purpose — the window stretches, the plan is cheaper, nothing breaks. It is a bad one to make by accident and then file as a bug against the API.
One note on the workload, since it decides everything above: the volumes worth planning for are the lawful ones — your own QA and staging suites, accessibility testing, uptime monitoring, and data collection that honours robots.txt, published rate limits and terms of service. Rate limits are a sizing input rather than an obstacle: a crawler capped at two requests per second has a peak arrival rate bounded by that cap, which makes the arithmetic smaller and the answer cheaper.
Measuring real peak concurrency
If a pipeline is already running, stop estimating. Log two timestamps per task — submitted, answered — and compute the concurrency curve with a sweep line:
def peak_concurrency(intervals, percentile=0.99):
"""intervals: list of (submitted_at, resolved_at) as epoch seconds."""
events = []
for start, end in intervals:
events.append((start, +1))
events.append((end, -1))
# Ties: -1 sorts before +1, so a task ending exactly as another
# starts does not inflate the count by one.
events.sort()
current, samples = 0, []
for _, delta in events:
current += delta
samples.append(current)
samples.sort()
idx = min(len(samples) - 1, int(len(samples) * percentile))
return {"peak": samples[-1], "p99": samples[idx]}
Size to the p99, not the absolute maximum. The single highest sample is usually one unlucky second — a coincidence of retries that a small queue absorbs invisibly — and paying monthly for a spike that lasts a second a week is waste. Take the p99, add roughly 20% headroom for growth and retry storms, round up to the nearest tier.
Your poll interval is part of the in-flight time
This is the detail that quietly inflates concurrency measurements. CaptchaAI's documented pattern is submit-then-poll: POST /in.php returns a task ID, and you poll /res.php about every 5 seconds until it stops answering CAPCHA_NOT_READY.
curl -s https://ocr.captchaai.com/in.php \
-d key=YOUR_API_KEY \
-d method=userrecaptcha \
-d googlekey=SITE_KEY_FROM_THE_PAGE \
-d pageurl=https://staging.example.com/contact \
-d json=1
# {"status":1,"request":"2122988149"}
# Poll — roughly every 5s
curl -s "https://ocr.captchaai.com/res.php?key=YOUR_API_KEY&action=get&id=2122988149&json=1"
# {"status":0,"request":"CAPCHA_NOT_READY"}
# {"status":1,"request":"03AGdBq26..."}
(The submit parameters differ per challenge type — check the docs for the exact field names before you wire one up. The submit-then-poll shape and the json=1 response envelope are the same for all of them.)
The slot in your worker is held until you observe the answer, not until the answer exists. With a 5-second poll interval, a task that finished in 0.4s still occupies your worker for up to 5 seconds — for image CAPTCHAs published at <0.5s, that is an order of magnitude of pure polling overhead, and it makes measured concurrency look roughly ten times worse than the work requires.
For fast types, use webhook delivery so the slot frees when the answer arrives rather than when your timer next fires. For slow types like reCAPTCHA v2, the 5-second cadence is noise against a 60-second solve — leave it alone and poll. Either way, know whether you are measuring the work or your own timer.
Turning the number into a plan
CaptchaAI's published tiers give three anchor points on the ladder:
| Plan | Price | Threads | Cost per thread |
|---|---|---|---|
| BASIC | $15/month | 5 | $3.00 |
| ADVANCE | $90/month | 50 | $1.80 |
| ENTERPRISE | $300/month | 200 | $1.50 |
Cost per thread is the arithmetic worth carrying: it falls as you climb, so scaling up is sublinear — ADVANCE is 10× the threads of BASIC for 6× the price. The full ladder has intermediate rungs (STANDARD, PREMIUM, CORPORATE, VIP-1/2/3) whose figures live on the pricing page; these three show the direction.
The published effective rates — ADVANCE from about $0.09 per 1,000 solves, ENTERPRISE from about $0.030 — need a careful reading, because they are not a price you are charged. There is no per-solve fee. They describe what the flat monthly fee works out to if you keep the threads busy. Divide and the implied volumes appear: $90 at $0.09/1K is about a million solves a month, $300 at $0.030/1K about ten million.
Reachable? Fifty threads over a 30-day month is roughly 130 million thread-seconds, so a million solves is about 130 seconds of budget each — double the slowest published solve time, meaning ADVANCE hits its headline rate at under half duty cycle even on reCAPTCHA v2. Two hundred threads is about 518 million thread-seconds, so ten million solves is around 52 seconds each: achievable, but only on a mix weighted toward faster types.
That is the honest shape of thread pricing. The headline cost per thousand is a function of your utilisation, not of the plan — an idle thread costs the same as a saturated one, which is why the number you want before opening the pricing page is your measured p99 concurrency, not your monthly solve count.
Enforce the ceiling on your side
Once you know your thread count, bound the client to it deliberately. A local semaphore means you queue with behaviour you understand:
import asyncio
PLAN_THREADS = 50
slots = asyncio.Semaphore(PLAN_THREADS)
async def solve(session, payload):
async with slots: # never exceed the plan's concurrency
task_id = await submit(session, payload)
return await poll_until_ready(session, task_id, interval=5)
Instrument the wait time on that semaphore. Queueing near zero means headroom; queueing that grows through your peak window means the workload genuinely wants the next tier. That signal, not a hunch, should trigger an upgrade. Retries count too — ERROR_UNSOLVABLE holds a slot for the full in-flight duration and returns nothing, so a retry loop is a concurrency consumer and belongs in the arithmetic. Our guide to running a CAPTCHA API in production covers the error taxonomy and backoff strategy that keeps retries from eating the plan you just sized.
Frequently asked questions
How many threads do I need for 10,000 CAPTCHAs a day?
There is no answer without the challenge type and the arrival pattern. Image CAPTCHAs (published <0.5s) arriving evenly are about 0.12 solves per second — well under one thread. reCAPTCHA v2 (published <60s) with 70% landing in a three-hour window needs roughly 40 threads at peak. Same daily volume, two very different plans.
Is thread-based pricing cheaper than paying per solve?
It depends on utilisation and challenge mix. With no per-CAPTCHA fee, a thread you keep busy gets steadily cheaper per solve while an idle one costs full price. Steady, high-volume, fast-challenge workloads favour threads; sporadic low-volume work on slow challenge types holds slots open longer and benefits less.
What if my peak is far higher than my average?
Two levers: buy for the peak, or flatten it. Staggering scheduled jobs, spreading crawl windows and letting a queue smooth arrivals is usually the cheaper of the two, and it keeps you comfortably inside the target site's published rate limits — which you should be respecting anyway.
Does my polling interval affect how many threads I need?
It affects how many your client consumes. A worker slot is held until you observe the answer, so a 5-second cadence adds up to 5 seconds of occupancy per task: negligible against a 60-second reCAPTCHA v2, enormous against a sub-second image CAPTCHA. For fast types, webhook delivery frees the slot on arrival rather than on your next timer tick.
Size it, then buy it
Sizing a CAPTCHA plan is arithmetic, not intuition: peak arrival rate times in-flight duration, capped by your own worker count, checked against a measured p99, plus headroom. Do that first and the plan picks itself — and you will know, before the first invoice, whether the next tier buys you anything.
When you have the number, see how the thread tiers line up and take the smallest plan that clears your peak.