SurSolver

BlogCAPTCHA Solving Costs: What Drives Them and How to Cut Them

CAPTCHA Solving Costs: What Drives Them and How to Cut Them

· Updated · 8 min read

Your captcha-solving bill is price per task multiplied by the number of tasks you submit. The price is fixed by the service. The number of tasks is the part your code controls. This post shows where tasks get wasted, how to cut them, and how to watch your balance so a bad run stops itself.

There are no competitor prices here. Prices change, and a number copied from another vendor's page is out of date by the time you read it. The method works whatever the rates are.

How SurSolver charges

These rules come from the Billing & packages and Task lifecycle guides:

  • A task is charged when it is accepted, that is, when createTask answers errorId: 0. It is not charged per successful solve.
  • If a task finishes as failed, the original charge is refunded automatically once. Successful tasks keep their charge. Contact support with the taskId if billing looks wrong.
  • A refused submission creates no task and costs nothing. That covers ERROR_ZERO_BALANCE, ERROR_NO_SLOT_AVAILABLE, ERROR_INVALID_REQUEST and ERROR_TYPE_NOT_SUPPORTED.
  • A retry is a new task. The solver API has no idempotency header, so a resubmission is charged again.
  • The price of one accepted task per service is the live rate sheet: GET /v1/services (no key needed, field price_per_request) or the pricing page.
  • For high volume there are no published tiers. Contact support to agree custom rates.

The consequence is simple. Every task you submit and later throw away is money spent.

What drives the bill

Most waste comes from submitting tasks that could not have produced a usable result. These are the usual causes, each of which the docs let you check:

  • The wrong task type. A Turnstile element and a Cloudflare challenge page are different products with different task types. The task for the wrong one is accepted and charged. The Turnstile guide explains how to tell them apart.
  • Parameters that are well formed but wrong. The API validates that required fields are present and the proxy parses. The docs describe no check that a websiteKey belongs to the page you named, so treat a wrong key as an accepted, charged, failing task.
  • A misread proxy string. A four-part proxy string is always read as user:password:host:port. 203.0.113.10:8000:user:12345 parses, is charged, and runs through the wrong proxy. Use the split proxyAddress/proxyPort fields to avoid it. See Proxies.
  • Tokens that expire before use. Tokens are single-use. Vendor documentation puts a reCAPTCHA token at about two minutes and a Turnstile token at about five. A task solved ahead of time and used late is wasted.
  • Blind retries. After ERROR_FAILED the task is final and was charged. A loop that resubmits the same bad parameters pays every time.
  • Resubmitting after a server fault. On HTTP 500, ERROR_INTERNAL_ERROR or a dropped connection you may not know whether the task was accepted. Resubmitting without checking can charge twice.
  • Solving when nothing was asked. Submitting a task for every page, including pages that never showed a challenge.
  • Re-solving what you could reuse. Covered below.

Measure cost per outcome

Price per task is the wrong number to optimise. The number that matters is cost per outcome you actually wanted:

snippet.txt
cost per useful page = (accepted tasks x price per task) / pages the target served

A ready status means the solver returned something. It does not mean the target site accepted it. Count the outcome on the target side: the form posted, the page loaded, the cookie worked. If you only count ready, a wrong-parameter problem shows up as a normal-looking bill.

Keep one CSV or spreadsheet row per task: taskId, task type, target host, time submitted, final status or errorCode, and whether the target accepted the result. Group the rows by host and task type to find where spend fails to produce useful pages.

Pre-filter before you submit

The cheapest task is the one you never submit.

  • Confirm a challenge is actually present. For a widget, check that the element is on the page and its field is empty. For a challenge page, check the response that triggered it: the first request answering HTTP 403 with an interstitial is a challenge, a normal page is not.
  • Validate parameters locally. Reject a task before sending it if websiteURL is not the page that renders the widget, if websiteKey is empty, or if a proxy string has the wrong number of parts.
  • Deduplicate. If three workers hit the same URL at once, solve once for the one that needs it. Do not submit one task per request.
  • Cap attempts per target. Count tasks spent per host per hour, and stop that target when the cap is hit. A target that keeps rejecting valid-looking results needs investigation, not more spend.

Reuse what is reusable

Tokens are single-use. You cannot cache them or pool them ahead of time. Cookies are different. A Cloudflare challenge returns a cookie jar, and vendor documentation says one solve covers the whole origin for roughly 30 minutes to a few hours, as long as you replay it from the same exit IP with the same userAgent. Cloudflare may revoke it early. Treat those figures as vendor claims, not guarantees.

In practice: store the jar keyed by host, proxy and user agent. Reuse it for every request through that proxy. Solve again only when the target challenges you again. The proxy rules are in Proxies; see how to stop getting blocked for keeping an identity stable so the challenge does not come back.

Retry on a budget

Failures are part of the job, so spend on them deliberately.

  • Stop polling at the first errorId: 1. The answer will not change.
  • Retry a failed task only after you change something: the parameters, the proxy, or the timing. The same input gives the same result.
  • Allow a small, fixed number of attempts per page. Drop the page or alert a human beyond that.
  • ERROR_NO_SLOT_AVAILABLE costs nothing. Wait for the (retry in Ns) hint, add jitter, and retry. See Rate limits.

Watch the balance with getBalance

POST /getBalance takes your clientKey and returns {"errorId":0,"balance":9.987}, the cash balance in USD. Poll it on a timer and alert below a floor you choose. Do not use it for accounting: it returns a JSON number, so it is fine for a threshold check and wrong for exact arithmetic. For exact amounts use GET /v1/balance (a decimal string, session auth).

snippet.ts
const API = 'https://api.sursolver.com';
const ALERT_FLOOR_USD = 5;
const CHECK_INTERVAL_MS = 5 * 60 * 1000;

interface BalanceReply {
  errorId: number;
  errorCode?: string;
  errorDescription?: string;
  balance?: number;
}

async function getBalance(clientKey: string): Promise<number> {
  const res = await fetch(`${API}/getBalance`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ clientKey }),
  });
  if (!res.ok) throw new Error(`getBalance transport failure: HTTP ${res.status}`);
  const body = (await res.json()) as BalanceReply;
  if (body.errorId !== 0 || typeof body.balance !== 'number') {
    throw new Error(`getBalance failed: ${body.errorCode ?? 'unknown'}`);
  }
  return body.balance;
}

async function watchBalance(clientKey: string): Promise<void> {
  for (;;) {
    try {
      const balance = await getBalance(clientKey);
      if (balance < ALERT_FLOOR_USD) {
        console.error(`balance ${balance} is below ${ALERT_FLOOR_USD}: pause or top up`);
      }
    } catch (err) {
      console.error(String(err));
    }
    await new Promise((resolve) => setTimeout(resolve, CHECK_INTERVAL_MS));
  }
}

void watchBalance(process.env.SURSOLVER_KEY ?? '');

The same call is useful after a server fault. If you cannot tell whether a createTask was accepted, compare the balance before and after before you resubmit.

For per-task numbers, task.completed and task.failed webhook payloads carry cost and latency_ms. GET /v1/usage?granularity=day&format=csv and GET /v1/transactions give the account-level view with a session token. These endpoints are in the API reference.

Packages and build-versus-buy

A package is a prepaid bundle: a fixed price, a pool of solves, a duration and a set of covered services. Each accepted task spends one solve, whatever the service price is, and a failed task restores one solve without extending or reactivating the plan. To compare, divide the package price by its solve quota and compare the result with price_per_request for the services you really use. Include the solves you will not use before it expires. Packages are on the pricing page only while they are on offer.

Building your own solver is a separate decision. Count engineering time, maintenance when the widget changes, and the proxies and browser infrastructure it needs. This post has no numbers for that, and you should be wary of anyone who offers a precise one without your workload.

Next steps

Start with the log table above. One week of rows shows which target and which task type waste tasks. Then read Billing & packages for the exact charging rules, and use how to choose a CAPTCHA solving service if you are weighing options.

More articles