A solver that works in a notebook and a solver that survives production differ in one place: what happens when the answer is not there yet. CaptchaAI documents distinct codes - CAPCHA_NOT_READY, ERROR_UNSOLVABLE, ERROR_ZERO_BALANCE - and branching on them properly is most of the reliability work.
Treat every non-success identically and you get one of three bugs. Tight retries hammer the endpoint and burn threads on work already in progress. Swallowing codes hides a billing state until the queue is empty. Waiting forever on a task that will never resolve stalls a worker that looks healthy.
Four rules, each mapped to a documented response.
This is the normal in-progress state, not an error. Poll res.php with action=get&id=<taskId> at the documented ~5s cadence. Resubmitting the task instead creates a second job and occupies a second thread for the same work.
A genuine terminal state for that task. Retry once if your workload tolerates it, then move on and record it. Infinite retries on an unsolvable task convert one failed item into a stalled pipeline.
Backoff is exactly wrong here: nothing will improve with time. This needs a human or an automated top-up. Route it to alerting rather than folding it into the generic retry path where it will look like a slow day.
Each type has a published latency band - under 0.5s for image, up to 60s for reCAPTCHA v2. Set your ceiling from the band for the type you are solving, with headroom, rather than one arbitrary global timeout.
The four numbers that tell you whether this is healthy.
Compare against the published band for that type. Drift upward is the earliest warning that something changed on the target side.
A rise concentrated on one site usually means that site changed its challenge, not that the solver degraded.
If in-flight tasks sit at your thread ceiling, you are queueing. That shows up as end-to-end latency long before it shows up as an error.
Climbing polls per solve means tasks are taking longer. Cheap to record and it moves before anything else does.
Reliability here is observable. Track these and you see a change on the target's side days before it becomes an incident.
The practical case, in four lines.
Polling an in-progress task instead of resubmitting keeps a second thread free for real work.
A zero-balance response routed to alerting rather than backoff stops a silent queue drain.
Treating unsolvable as terminal for that item keeps one bad task from stalling the pipeline.
Per-type published bands give a defensible ceiling instead of one global guess.
Generally no for the in-progress poll - the documented pattern is a steady ~5s cadence until the result is ready. Reserve backoff for transport-level failures like a network error or a 5xx, not for CAPCHA_NOT_READY.
Once, if at all, and only when a duplicate solve is harmless. It is a terminal state for that task; the useful response is to record it and watch the rate.
Persist the task id when you submit, not just in memory. On restart you can resume polling an existing task rather than paying for the same solve twice.
It helps when the target fingerprints the IP the solve originates from. Tasks accept proxy and proxytype so the solve can share your session's network context.
If individual solve times match the published bands but end-to-end latency is rising, you are queueing and need concurrency. If solve times themselves are drifting, the target or the type has changed.
Branch on what the API actually tells you, and the 3am pages stop.