Solve CAPTCHA tasks concurrently in Go
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/sdkmodule at versionv0.1.0. Installation details are in the Go integration documentation. - Set
SOLVETHREE_API_KEYandSOLVETHREE_PROXYin the server environment. Keep the API key out of browser code. - Replace
accounts.example.comandloginwith 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.
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, notresult.ID. - Use
SuccessfulProxywith the original task and ready result to identify the successful proxy. Submit through that proxy with the returnedUserAgentandSecChUAbrowser 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.Errorto obtainRequestIDandTaskID. If the task ID is known, resume retrieval withGetTaskunder 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, recordErrorCodeand retrieve the failed task if you need itsErrortext. 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.
Sources and API reference
Run your first task.
Start with $5 in workspace credit.