How to Solve CAPTCHAs in Parallel with Python asyncio

If you need to solve captchas in parallel from Python, use AsyncCapSkip with asyncio.gather. Python is the one CapSkip SDK where the async client is a real async implementation rather than an alias, so a batch of ten reCAPTCHAs finishes in roughly the time of the slowest one instead of the sum of all ten. This guide covers the working code, how to cap concurrency so you do not swamp the solver, and how to stop one failure killing the batch.
Why the Python client is the interesting one
All four SDKs export something called AsyncCapSkip. Only one of them is a separate implementation.
| SDK | What AsyncCapSkip is | How you run work concurrently |
|---|---|---|
| Python | A genuine async client | await asyncio.gather(...) |
| Node.js | An alias of CapSkip | await Promise.all([...]) |
| .NET | An alias of CapSkipClient | await Task.WhenAll(...) |
| PHP | An alias only, for source parity | Synchronous, no concurrency |
Node and .NET are already non-blocking, so the alias costs nothing there. PHP is synchronous and the alias buys you nothing at all. In Python the distinction matters: the plain CapSkip client blocks the event loop while it polls, so putting it inside a coroutine gives you concurrency on paper and sequential timing in practice.
What you need
- Python 3.10 or newer
pip install capskip- CapSkip running with its API server on, listening on
127.0.0.1:8080 - A list of sitekeys and page URLs to work through
Nothing leaves your machine, so there is no rate limit to negotiate and no per-solve meter running while you experiment. Full method signatures for every type live on the CAPTCHA solving SDK page.
The sequential version, and what it costs
Here is the shape most people start with. It is correct, and it is slow.
# pip install capskip
from capskip import CapSkip
solver = CapSkip(host="127.0.0.1", port=8080)
targets = [
("SITEKEY_A", "https://example.com/page-a"),
("SITEKEY_B", "https://example.com/page-b"),
("SITEKEY_C", "https://example.com/page-c"),
]
# Each call blocks until that one CAPTCHA comes back.
for sitekey, url in targets:
result = solver.recaptcha(sitekey=sitekey, url=url)
print(result["code"][:40]) # the token, truncated for the logA reCAPTCHA solve is mostly waiting. Your process sits idle while the solver works, then moves to the next one and sits idle again. Three solves take three solves’ worth of wall clock. Thirty take thirty.
Solve several CAPTCHAs at once with asyncio.gather
Swap the client, await the calls, and hand them all to gather. The types can be mixed: reCAPTCHA, Turnstile and GeeTest in the same batch is fine.
# pip install capskip
import asyncio
from capskip import AsyncCapSkip
async def main():
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
# gather starts all three now and waits for the slowest.
results = await asyncio.gather(
solver.recaptcha(sitekey="SITEKEY_A", url="https://example.com/page-a"),
solver.turnstile(sitekey="SITEKEY_B", url="https://example.com/page-b"),
solver.normal("captcha.png"),
)
for r in results:
print(r["code"][:40]) # token for widgets, text for images
asyncio.run(main())Every method returns a dict with the same core fields: captchaId and code. Turnstile adds userAgent, which you must send back with the token when you submit a challenge-page solve. GeeTest adds challenge, validate and seccode, and puts the raw JSON in code.
Cap the concurrency with a semaphore
Do not fire two hundred solves at a local daemon and hope. Solving is CPU work happening on your own machine, so past a certain width you are just queueing against yourself and every individual solve gets slower. A semaphore keeps a fixed number in flight.
import asyncio
from capskip import AsyncCapSkip
# Start at 4 or 5, then measure. More is not automatically faster.
sem = asyncio.Semaphore(5)
async def solve_one(solver, sitekey, url):
async with sem:
return await solver.recaptcha(sitekey=sitekey, url=url)
async def run(targets):
solver = AsyncCapSkip()
tasks = [solve_one(solver, k, u) for k, u in targets]
return await asyncio.gather(*tasks)Pick the number by timing it, not by guessing. Run the same 20 targets at 2, 5 and 10 and keep whichever finishes first on your hardware. The right answer depends on your CPU, not on the SDK.
One client, not one per task
Build a single AsyncCapSkip and share it across coroutines, as above. Constructing one per task is wasteful and gains you nothing: the client holds configuration, not per-solve state.
Stop one failure killing the batch
By default gather propagates the first exception and you lose the results of everything else that was in flight. Pass return_exceptions=True and the exceptions arrive as ordinary items in the results list, so you can sort the winners from the losers.
from capskip import (
AsyncCapSkip, ApiException, NetworkException,
TimeoutException, ValidationException,
)
results = await asyncio.gather(*tasks, return_exceptions=True)
for target, r in zip(targets, results):
if isinstance(r, TimeoutException):
print("timed out, worth retrying:", target)
elif isinstance(r, ApiException):
print("api rejected this one:", target, r)
elif isinstance(r, NetworkException):
print("solver unreachable, stop the run:", target)
elif isinstance(r, Exception):
raise r
else:
print("ok:", r["code"][:40])The four exception types are the same in every CapSkip SDK, and all of them derive from a base CapSkipError if you would rather catch one type. ValidationException means your parameters are wrong and a retry will fail identically. NetworkException usually means the app is not running, which is a whole-run problem rather than a per-target one.
Timeouts and polling, which behave differently per type
Two separate timeouts apply, and a batch that mixes types is governed by both.
| Option | Default | Applies to |
|---|---|---|
defaultTimeout | 120 seconds | Image CAPTCHAs |
recaptchaTimeout | 300 seconds | reCAPTCHA, Turnstile, GeeTest |
pollingInterval | 5 seconds | The maximum gap between polls |
pollingInterval is worth understanding before you tune it. The SDK does not poll on a flat interval. It starts at 250ms and backs off towards the value you set, which is why an SDK solve usually returns sooner than a hand-rolled loop built from the raw API’s “wait, then poll every five seconds” advice. Raising it makes fast solves land later. Lowering it adds request volume for no gain.
GeeTest challenges expire, so do not pre-build a batch
This one bites specifically when you go parallel. A GeeTest gt value is static per site, but challenge is single-use and expires in about a minute. If you collect fifty challenges first and then start solving, the ones at the back of the queue are dead before they are submitted. Fetch each challenge immediately before the solve that uses it.
Full working example
# pip install capskip
import asyncio
from capskip import AsyncCapSkip, ApiException, TimeoutException
TARGETS = [
("SITEKEY_A", "https://example.com/page-a"),
("SITEKEY_B", "https://example.com/page-b"),
("SITEKEY_C", "https://example.com/page-c"),
]
async def solve_one(solver, sem, sitekey, url):
async with sem:
return await solver.recaptcha(sitekey=sitekey, url=url)
async def main():
solver = AsyncCapSkip(host="127.0.0.1", port=8080)
sem = asyncio.Semaphore(5)
tasks = [solve_one(solver, sem, k, u) for k, u in TARGETS]
results = await asyncio.gather(*tasks, return_exceptions=True)
tokens = {}
for (sitekey, url), r in zip(TARGETS, results):
if isinstance(r, (ApiException, TimeoutException)):
print("failed:", url, r)
else:
tokens[url] = r["code"]
print(len(tokens), "of", len(TARGETS), "solved")
return tokens
asyncio.run(main())That is the whole pattern: one shared client, a semaphore, return_exceptions=True, and a dict of tokens at the end. Drop it into a scraper and the CAPTCHA step stops being the bottleneck. The CAPTCHA solver for web scraping page covers where it fits in a wider pipeline.
The same idea in the other SDKs
If you are porting this, the concurrency primitive changes but the shape does not. Node is already non-blocking, so the plain client is all you need.
// npm install capskip
const { CapSkip } = require('capskip');
const solver = new CapSkip({ host: '127.0.0.1', port: 8080 });
// AsyncCapSkip here is just an alias. Promise.all does the work.
const results = await Promise.all([
solver.recaptcha('SITEKEY_A', 'https://example.com/page-a'),
solver.turnstile('SITEKEY_B', 'https://example.com/page-b'),
]);
console.log(results.map(r => r.code));.NET is the same story with Task.WhenAll, and PHP has no concurrency to offer at all. If you need parallel solving and you get to choose the language, Python is the one with the purpose-built client. The Python CAPTCHA solver page has the rest of the surface.
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
Using CapSkip inside coroutines | Blocks the event loop, so timing stays sequential | Use AsyncCapSkip |
| No semaphore | Every solve slows down once the queue is deep | Cap in-flight work, start around 5 |
Plain gather | One failure discards every other result | return_exceptions=True |
| Pre-fetching GeeTest challenges | Later ones expire before submission | Fetch each one just before solving |
Raising pollingInterval | Fast solves return later, not sooner | Leave it at the default |
| Proxy set on an image solve | Not supported for image CAPTCHAs | Proxies apply to reCAPTCHA, Turnstile and GeeTest only |
The raw request and response for each type, if you want to see what the SDK is sending, is in the API documentation. Python’s own asyncio task reference covers gather semantics in detail.
Frequently asked questions
How many CAPTCHAs can I solve at once?
There is no quota to hit, so the limit is your own hardware. Solving happens locally, so concurrency is bounded by CPU rather than by an account tier. Start at five in flight, time a fixed batch, and adjust from there.
Can I mix CAPTCHA types in one gather call?
Yes. recaptcha, turnstile, geetest and normal are all coroutines on the same client and can be awaited together. Remember that image solves use the 120 second timeout while the rest use 300.
Should I use threads instead?
Only if your surrounding code is already threaded. The work is I/O bound waiting, which is exactly what asyncio is for, and one event loop is cheaper than a thread pool. If you are stuck on a sync codebase, a thread pool around the plain CapSkip client works too.
Does AsyncCapSkip need to be closed?
The SDK documents no close method or async context manager, so build one client, use it for the run, and let it go out of scope when the process ends.
Summary
Use AsyncCapSkip, share one client, cap in-flight solves with a semaphore, and pass return_exceptions=True so a single bad target does not discard the batch. That turns a queue of CAPTCHAs from a serial bottleneck into one wait.
The reason you can widen concurrency freely is that the solver runs on your own machine. There is no per-solve bill and no shared queue to share with strangers, so scaling out is a question of your CPU rather than someone else’s rate limit. That is the practical difference a local captcha bypass tool makes once your batch sizes stop being small.
