Nobody's CAPTCHA integration fails during the demo. The task submits, the token comes back, the form accepts it, everyone moves on. The failure comes three weeks later, at some indefensible hour, when a batch job has been retrying ERROR_ZERO_BALANCE for six hours, the semaphore is full of doomed tasks, and the dashboard — if there is a dashboard — shows a success rate nobody defined precisely enough to trust.
The difference between those two moments is not the code that talks to the API. It is everything wrapped around that code. You already treat your database and your payment provider as dependencies: their errors are classified, their timeouts bounded, their health graphed, their failures rehearsed. A solving API deserves exactly the same treatment, and this guide is the map of what that means — with pointers into the deeper treatments each piece has earned on this blog.
Key takeaway: production reliability here is five disciplines, in order. Classify every response into one of three categories. Bound every wait with a deadline that actually cancels. Retry only what can succeed, with backoff and jitter. Size and protect concurrency deliberately. Graph two success rates, not one — and alert on the handful of conditions that deserve a human.
If you have not wired the submit–poll–inject pattern yet, start with the integration guide; everything below assumes that loop exists and works on a good day.
Discipline 1: the three-way error taxonomy
Every response your poll loop can receive falls into one of three categories, and every production incident in this space traces back to code that confused two of them.
Transient, per-task. CAPCHA_NOT_READY is the only member that matters, and it is not an error at all — it is a state. The task is in flight; the correct handling is to keep waiting on the documented ~5-second cadence until your deadline says otherwise. It arrives with HTTP 200 and status: 0, which is why the body, never the HTTP code, is your signal.
Terminal, per-task. ERROR_UNSOLVABLE means this task is finished and no amount of polling changes that. The task failed; the account, the key, and every other task in flight are fine. Correct handling: resubmit at most once as a fresh task, then let the work item fail upward to whatever owns it.
Terminal, systemic. ERROR_ZERO_BALANCE and ERROR_WRONG_USER_KEY say nothing about the task and everything about the account. Every task you submit while the condition holds will fail identically. Correct handling: stop submitting, page a human, and hold the queue. This is the category that turns into a 3am retry storm when it is handled like the per-task kind — thousands of doomed submissions, all burning threads, none capable of succeeding.
The classifier is small enough that there is no excuse for branching on raw strings all over the codebase:
TRANSIENT = {"CAPCHA_NOT_READY"}
TERMINAL_TASK = {"ERROR_UNSOLVABLE"}
TERMINAL_SYSTEMIC = {"ERROR_ZERO_BALANCE", "ERROR_WRONG_USER_KEY"}
def classify(resp: dict) -> str:
if resp.get("status") == 1:
return "solved"
code = resp.get("request", "")
if code in TRANSIENT:
return "wait"
if code in TERMINAL_SYSTEMIC:
return "halt_everything"
return "fail_task" # ERROR_UNSOLVABLE and anything unrecognized
Note the default: an error code you have never seen before is treated as terminal for the task, not retried on faith. Unknown errors that turn out to be retryable cost you one solve; unknown errors retried in a loop cost you the night.
Discipline 2: deadlines that actually cancel
Every task needs a deadline, the deadline needs to fit the challenge type, and hitting it must cancel the work — three separate requirements, routinely implemented as one vague timeout.
Per-type budgets come straight from the vendor's published envelopes: CaptchaAI publishes >99.5% success in under 60 seconds for reCAPTCHA v2, >99% in under 4 seconds for v3, 100% in under 10 seconds for Turnstile, and sub-0.5-second times on image CAPTCHAs. Those are planning figures, not guarantees for any individual task — so a sane deadline is the published envelope plus real slack: a global 30-second timeout is simultaneously too tight for v2 and absurdly loose for image work. Set deadlines per method, and store the submission timestamp with the task ID so the check is trivial.
The cancellation half is the part that bites. A deadline that abandons the result but leaves the polling coroutine running — or worse, resubmits while the original is still in flight — has not freed anything: the slot is still held, and your effective concurrency quietly shrinks under exactly the load that made tasks slow in the first place. This is one of the seven patterns dissected in the retry loop is where CAPTCHA budgets burn; the short version is that "timed out" must mean the slot is released and the task will not be double-counted if the token lands late.
Discipline 3: a retry policy you can write on one line
After the taxonomy, retry policy nearly writes itself: retry nothing transient (wait instead), retry a terminal task once, retry systemic failures never — and jitter anything scheduled.
Two additions make it production-grade. First, distinguish solver failure from downstream failure before retrying anything: a token that came back fine but was rejected by the target is not a solving problem, and resubmitting the CAPTCHA spends money on the wrong diagnosis — the mismatch usually points at network context, which is the proxy question. Second, when many tasks fail together, back off together with jitter rather than in synchronized waves. Synchronized retries are how a hiccup becomes an outage, on your side and on everyone else's.
The full failure catalogue — retrying downstream failures, uncancelled timeouts, retries that inflate concurrency, counting retried successes as first-try health — lives in the retry-mistakes guide. Treat it as the test suite for whatever policy you write.
Discipline 4: concurrency is a budget, not a suggestion
Thread-based pricing means your plan is a fixed number of in-flight slots — no per-solve cap within a thread, but a hard ceiling on simultaneity. Production discipline is a client-side semaphore sized to that number, wrapped around the entire submit-to-resolution span, not just the submission call.
Everything above interacts with this budget. An uncancelled timeout holds a slot. A retry holds a slot for a full second pass and delivers at most one token for two slots' worth of occupancy. A systemic error handled as per-task fills the entire semaphore with tasks that cannot succeed, which reads as "the API got slow" on your metrics while every slot burns on nothing.
The operational signal to watch is queue wait at your own semaphore — time between a work item wanting a slot and getting one. Near-zero queueing means headroom; queueing that grows through your peak window means the workload genuinely needs the next tier, and that arithmetic — Little's Law, peak-versus-average, the 100× spread in solve times across challenge types — is the subject of sizing threads. Upgrade on that signal, not on a bad afternoon.
Discipline 5: graph two rates, and only page on what matters
The single most valuable monitoring decision is to record two success rates and never conflate them:
- Solver rate: of tasks submitted, how many returned a token before deadline.
- Acceptance rate: of tokens injected, how many the target accepted.
They fail independently, and the split is the diagnosis. Solver rate down: look at your parameters, your deadlines, your balance. Acceptance rate down with solver rate healthy: the target changed — a tightened threshold, new IP-reputation weighting, a different challenge — and why your solve rate dropped overnight is the structured walk through that fork. One blended number can hide a collapsing acceptance rate behind a healthy solver rate for days.
Around those two rates, a small fixed set of series covers nearly every incident: solve-time percentiles per challenge type (medians hide the tail that eats slots), an error counter per code — a classify() result is trivially a metrics label — semaphore queue wait, first-try success separate from success-after-retry, and account balance as a graphed metric with a threshold, because ERROR_ZERO_BALANCE should be an alert you saw coming, not one that finds you.
Which leads to alerting, where restraint is the discipline. Three conditions deserve a page: any systemic error appearing at all, balance crossing the threshold that gives a human time to act, and either success rate falling hard through a floor. Almost everything else — a slow tail, a drifting acceptance rate, growing queue wait — is a morning problem, and paging on it teaches the on-call to ignore the pager.
When it breaks anyway: the short playbook
A written five-line runbook beats improvisation at 3am:
- Check for systemic errors first. Balance and key problems explain everything downstream and take minutes to fix.
- Split the two rates. Solver-side or acceptance-side decides the entire branch that follows.
- Check the denominator. A "drop" on tiny volume, or a batch job flooding in retries, distorts both rates before anything is actually wrong.
- Change one variable at a time — deadline, proxy context, challenge parameters — and measure, rather than turning every knob and learning nothing.
- Degrade deliberately. Decide now what the pipeline does when solving is unavailable: queue and drain later, or skip and mark for a re-crawl. Either is fine; discovering at midnight that the answer is "crash" is not. A simple circuit breaker — stop submitting after N consecutive systemic failures — is a dozen lines and prevents the worst hour.
The quiet part: reliability is also politeness
Every discipline above has a second beneficiary: the site you are automating. Bounded deadlines, capped concurrency, jittered backoff and a circuit breaker are exactly what keeps your traffic inside published rate limits and robots.txt expectations when things go wrong — a client that fails gracefully is a client that does not hammer anyone. This applies to legitimate work — QA suites, monitoring, accessibility testing, lawful data collection under the target's terms — and none of it licenses attacking authentication, mass account creation, or fraud. The fuller argument is in what robots.txt, rate limits and terms actually require.
FAQ
Which CAPTCHA API errors are safe to retry?
Only per-task terminal errors like ERROR_UNSOLVABLE, and only once, as a fresh task. CAPCHA_NOT_READY is a wait state, not a failure; account-level errors like ERROR_ZERO_BALANCE fail every future task identically, so retrying them is pure waste.
What timeout should I set for a CAPTCHA solve? Per challenge type, from the vendor's published envelope plus slack — well past 60 seconds for reCAPTCHA v2, far tighter for v3 and image work. And make the timeout cancel the task's slot; a deadline that abandons the result but keeps the slot shrinks your real concurrency.
Why did my success rate drop with no deploy on my side? First confirm which rate dropped. Solver rate points at parameters, deadlines, or balance; acceptance rate with a healthy solver rate points at the target changing — threshold, challenge type, or IP-reputation weighting. The two-rate split is the diagnosis.
Do I need real monitoring for a small workload? You need the two success rates, an error counter per code, and a balance alert — a few counters, not an observability stack. Small workloads fail the same ways large ones do; they just fail unwatched for longer.
What should happen when the solver is unreachable? Whatever you decided in advance: queue and drain, or skip and mark for retry later — behind a circuit breaker so the pipeline stops submitting instead of stampeding. The wrong answer is the one you improvise during the outage.
Closing
The demo proves the loop works; production is about the wrapper — a three-way classifier, deadlines that cancel, a one-line retry policy, a semaphore treated as a budget, and two rates on a graph with three alerts that matter. Build the wrapper once and the 3am page becomes a morning glance at a dashboard that already knows the answer. The documented error codes, per-type parameters, and published latency and success figures to size your deadlines against are at getcaptchaai.com.