Fast captcha solving: how to cut latency in your pipeline
A fast captcha solver is one you can call at the right moment, in parallel, without wasting requests. The Scarnox solve call returns when the token is ready, so its duration depends on the captcha type, the target site and current load. This guide does not promise a fixed time. It shows what you control in your own code, how to measure it and how to run many solves at once, so the time you do spend is spent well.
What sets the solve time
Three things matter, and only one of them is in your hands:
- The captcha itself. Some challenge types take longer than others. That is a property of the site's captcha, not of your code.
- Load on the solver. A busy period can make a single call slower. Retry logic should handle that, not abandon the call early.
- Your own pipeline. Solving too early, serialising every request and waiting on unrelated work all add time you can remove.
Focus your effort on the third item. It is the part you can measure and change.
Measure before you optimise
Wrap the call with a timer and log the result. You want to know the solve time and the time your code spends around it:
import time
import requests
API_KEY = "scarnox_YOURAPIKEY"
URL = "https://api.scarnox.com/api/tasks/create"
def timed_solve(sitekey: str, page_url: str) -> dict:
started = time.perf_counter()
r = requests.post(
URL,
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"captcha_type": "PopularCaptchaTaskProxyless",
"site_key": sitekey,
"site_url": page_url,
},
timeout=185,
)
elapsed = time.perf_counter() - started
data = r.json()
if data.get("errorId") != 0:
return {"ok": False, "error": data.get("errorCode"), "seconds": elapsed}
return {"ok": True, "token": data["solution"]["token"], "seconds": elapsed}
result = timed_solve("10000000-ffff-ffff-ffff-000000000001", "https://example.com/signup")
print(result["ok"], round(result["seconds"], 2), "s")
Run this against the pages you care about at the times you run your jobs. Keep the numbers. They tell you whether a change actually helped.
Run solves in parallel
Solves are independent. If you have ten sitekeys to handle, waiting for each one in turn wastes time that you could spend on the others. A thread pool is enough for most scripts because each solve is one HTTP request that spends most of its time waiting:
from concurrent.futures import ThreadPoolExecutor
jobs = [
("10000000-ffff-ffff-ffff-000000000001", "https://example.com/signup"),
("10000000-ffff-ffff-ffff-000000000001", "https://example.com/register"),
("10000000-ffff-ffff-ffff-000000000001", "https://example.com/contact"),
]
def run(job):
sitekey, page_url = job
return timed_solve(sitekey, page_url)
# Start small. Raise the worker count only while errors stay low.
with ThreadPoolExecutor(max_workers=5) as pool:
results = list(pool.map(run, jobs))
for r in results:
status = "ok" if r["ok"] else r["error"]
print(f"{r['seconds']:.2f} s {status}")
If you see errors that look like rate or capacity problems when you raise the worker count, lower it. Parallelism only helps when the server keeps up with it.
Do not solve early
The fastest code is the code that does not wait for a token it will not use yet. Requesting tokens in advance and storing them is the usual mistake. Tokens expire about two minutes after they are issued, so a stored token is often stale when you need it. Solve when the form is about to be posted. The detailed reasoning is in captcha token expired: causes and fixes.
Keep the rest of the path short
- Keep one HTTP session per site so you do not reconnect for each request.
- Do not run slow parsing or database work between the solve and the post.
- Use a sensible timeout on your own requests. The solve call uses about 185 seconds because it blocks until the token is ready. Your other calls should be much shorter.
- Send a proxy only when the task needs one. The proxy task types take a
proxyfield in the formathttp://user:pass@host:port, but adding a proxy is not a speed-up on its own.
Reuse what does not expire
Some work can be shared safely across solves. Keep these out of the hot path:
- Sitekeys. Read the sitekey once per page template and store it. It does not expire the way a token does. Re-read it only when the form starts failing.
- HTTP connections. A session with keep-alive avoids a new TLS handshake on every call. Create one session per thread or per worker, not one per request.
- Parsed configuration. Load your target list, proxy list and settings once at start-up instead of reading files inside the solve function.
These changes cost little and remove overhead that appears in every call.
Measure the tail, not the average
An average solve time hides the slow calls that hold up a batch. Record each solve time, then look at the median and the 95th percentile. A pipeline that finishes in the time of its slowest job is limited by the tail, so a small change to how you retry slow calls can matter more than any change to the median:
import statistics
def summarise(seconds: list[float]) -> str:
ordered = sorted(seconds)
p95 = ordered[int(0.95 * (len(ordered) - 1))]
return f"median {statistics.median(ordered):.2f} s, p95 {p95:.2f} s, n={len(ordered)}"
print(summarise([r["seconds"] for r in results]))
Compare the summary before and after each change. If the p95 barely moves, the time is being spent somewhere else in your code.
Check the result, not just the speed
A fast failure is still a failure. Count the solves that return an error, and treat a rising error rate as a signal to slow down. Failed solves are not charged, but the time spent on them is still time your pipeline lost. The full retry pattern is in captcha solver error handling.