Solve CAPTCHA tasks concurrently in Go

By solvethree3 min read

Use one shared Go SDK client and a fixed worker pool. Give each job its own context, then submit each ready solution immediately instead of collecting tokens for a later batch.

On this page

Prepare the client and target

  • Use Go 1.22 or newer and the official solvethree.com/sdk module at version v0.1.0. Installation details are in the Go integration documentation.
  • Set SOLVETHREE_API_KEY and SOLVETHREE_PROXY in the server environment. Keep the API key out of browser code.
  • Replace accounts.example.com and login with your target hostname and action. The workspace needs an active profile for that site; submitting the hostname doesn't create one. See site profile setup.

The example requests reCAPTCHA Enterprise, the default. It supplies one proxy and deliberately leaves Type and URL unset: reCAPTCHA requests use the site hostname and action, not those fields.

Bound the work, not the number of clients

The client is safe for concurrent use. This example starts four workers and feeds them eight jobs through an unbuffered channel. The producer waits when every worker is busy, rather than launching a goroutine for every job. Four is an illustrative application setting, not a solvethree service limit.

Go · four workers and immediate per-job handling
package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "sync"
    "time"
    solvethree "solvethree.com/sdk"
)

func main() {
    client, err := solvethree.NewClient(os.Getenv("SOLVETHREE_API_KEY"), nil)
    if err != nil { log.Fatal(err) }
    jobs := make(chan int)
    var workers sync.WaitGroup
    for worker := 0; worker < 4; worker++ {
        workers.Add(1)
        go func() {
            defer workers.Done()
            for job := range jobs {
                ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
                input := solvethree.TaskRequest{Site: "accounts.example.com", Action: "login", Proxy: os.Getenv("SOLVETHREE_PROXY")}
                result, err := client.Solve(ctx, input, solvethree.SolveOptions{})
                if err != nil { cancel(); log.Printf("Job %d: %v", job, err); continue }
                fmt.Println("Ready task:", result.ID)
                // Submit this solution now, with its proxy and browser identity.
                cancel()
            }
        }()
    }
    for job := 0; job < 8; job++ { jobs <- job }
    close(jobs)
    workers.Wait()
}

Each worker creates a separate 180-second context for each job. Solve creates the task once and waits for its result. Because the example omits RequestID, the SDK generates one for that creation. A failed call is logged and continue moves only that worker to its next job; it doesn't stop the batch.

Keep cancellation inside the loop, as shown, so each job releases its context resources promptly. Cancellation stops local waiting; it doesn't cancel remote work.

Handle the solution before taking another job

The example prints the ready task ID; it doesn't submit anything to the target. Replace the comment with your target request, then report that job's submission result before the worker takes another job. Don't append tokens to a slice for use after workers.Wait.

  • Submit result.Solution.Token, not result.ID.
  • Use SuccessfulProxy with the original task and ready result to identify the successful proxy. Submit through that proxy with the returned UserAgent and SecChUA browser identity.
  • Keep submission inside the job context, before cancel(). The 180-second deadline then covers both solving and submission; use a separate submission context if you need a separate budget.

Report solving and target submission separately. A ready task provides a usable solution, but only the target's response tells you whether it accepted the application request.

Recover an interrupted job without restarting the batch

Logging an error is enough for this small example, but a recoverable integration should retain each job's exact task inputs and available request and task IDs. A timeout doesn't prove that task creation failed.

  • Extract *solvethree.Error to obtain RequestID and TaskID. If the task ID is known, resume retrieval with GetTask under a fresh context.
  • If creation is uncertain and you have the request ID, replay the original inputs with that same RequestID. The same request returns the existing task; changing the inputs while reusing the ID conflicts.
  • For *TaskFailedError, record ErrorCode and retrieve the failed task if you need its Error text. Stop polling that task and address the cause rather than blindly submitting the same invalid inputs again.

Keep HTTP errors separate from terminal task failures. errors.As can extract a nested *APIError with StatusCode and RetryAfter. Solve already retries temporary polling responses with HTTP 429, 502, 503, or 504, honoring Retry-After within the context deadline. Task creation is never automatically retried.

Run your first task.

Start with $5 in workspace credit.

Create a workspace