API Tutorials

Async CAPTCHA Solving in C# with Task.WhenAll and CaptchaAI

C#'s async/await and Task.WhenAll make concurrent CAPTCHA solving straightforward. This tutorial shows how to submit multiple CAPTCHAs to CaptchaAI in parallel, poll for results concurrently, and collect all solutions — handling partial failures gracefully.

Prerequisites

dotnet new console -n CaptchaSolver
cd CaptchaSolver
dotnet add package System.Text.Json

No extra packages needed — HttpClient and Task.WhenAll are built into .NET.

Core CaptchaAI Client

using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

public class CaptchaAiClient : IDisposable
{
    private readonly HttpClient _client;
    private readonly string _apiKey;
    private const string SubmitUrl = "https://ocr.captchaai.com/in.php";
    private const string ResultUrl = "https://ocr.captchaai.com/res.php";

    public CaptchaAiClient(string apiKey)
    {
        _apiKey = apiKey;
        _client = new HttpClient();
    }

    public async Task<string> SolveCaptchaAsync(string sitekey, string pageurl)
    {
        // Submit
        var submitParams = new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("key", _apiKey),
            new KeyValuePair<string, string>("method", "userrecaptcha"),
            new KeyValuePair<string, string>("googlekey", sitekey),
            new KeyValuePair<string, string>("pageurl", pageurl),
            new KeyValuePair<string, string>("json", "1")
        });

        var submitResp = await _client.PostAsync(SubmitUrl, submitParams);
        var submitJson = await submitResp.Content.ReadAsStringAsync();
        var submitData = JsonSerializer.Deserialize<ApiResponse>(submitJson);

        if (submitData.Status != 1)
            throw new Exception($"Submit failed: {submitData.Request}");

        var captchaId = submitData.Request;

        // Poll for result
        for (int i = 0; i < 60; i++)
        {
            await Task.Delay(5000);

            var resultResp = await _client.GetAsync(
                $"{ResultUrl}?key={_apiKey}&action=get&id={captchaId}&json=1"
            );
            var resultJson = await resultResp.Content.ReadAsStringAsync();
            var resultData = JsonSerializer.Deserialize<ApiResponse>(resultJson);

            if (resultData.Status == 1)
                return resultData.Request;

            if (resultData.Request != "CAPCHA_NOT_READY")
                throw new Exception($"Solve failed: {resultData.Request}");
        }

        throw new TimeoutException("Solve timeout after 300s");
    }

    public void Dispose() => _client.Dispose();
}

public class ApiResponse
{
    public int Status { get; set; }
    public string Request { get; set; }
}

Parallel Solving with Task.WhenAll

public class BatchSolver
{
    private readonly CaptchaAiClient _client;

    public BatchSolver(string apiKey)
    {
        _client = new CaptchaAiClient(apiKey);
    }

    public async Task<BatchResult> SolveAllAsync(
        IReadOnlyList<CaptchaTask> tasks)
    {
        var solveTasks = new Task<TaskResult>[tasks.Count];

        for (int i = 0; i < tasks.Count; i++)
        {
            var task = tasks[i];
            solveTasks[i] = SolveSingleAsync(task);
        }

        // Wait for ALL tasks — no short-circuiting on failure
        var results = await Task.WhenAll(solveTasks);

        return new BatchResult
        {
            Solved = Array.FindAll(results, r => r.Solution != null),
            Failed = Array.FindAll(results, r => r.Error != null)
        };
    }

    private async Task<TaskResult> SolveSingleAsync(CaptchaTask task)
    {
        try
        {
            var solution = await _client.SolveCaptchaAsync(
                task.Sitekey, task.Pageurl);
            return new TaskResult
            {
                TaskId = task.TaskId,
                Solution = solution
            };
        }
        catch (Exception ex)
        {
            return new TaskResult
            {
                TaskId = task.TaskId,
                Error = ex.Message
            };
        }
    }
}

public record CaptchaTask(string TaskId, string Sitekey, string Pageurl);

public class TaskResult
{
    public string TaskId { get; set; }
    public string Solution { get; set; }
    public string Error { get; set; }
}

public class BatchResult
{
    public TaskResult[] Solved { get; set; }
    public TaskResult[] Failed { get; set; }
}

Controlling Concurrency with SemaphoreSlim

public async Task<BatchResult> SolveWithLimitAsync(
    IReadOnlyList<CaptchaTask> tasks,
    int maxConcurrency = 10)
{
    var semaphore = new SemaphoreSlim(maxConcurrency);
    var solveTasks = new Task<TaskResult>[tasks.Count];

    for (int i = 0; i < tasks.Count; i++)
    {
        var task = tasks[i];
        solveTasks[i] = ThrottledSolveAsync(task, semaphore);
    }

    var results = await Task.WhenAll(solveTasks);

    return new BatchResult
    {
        Solved = Array.FindAll(results, r => r.Solution != null),
        Failed = Array.FindAll(results, r => r.Error != null)
    };
}

private async Task<TaskResult> ThrottledSolveAsync(
    CaptchaTask task, SemaphoreSlim semaphore)
{
    await semaphore.WaitAsync();
    try
    {
        return await SolveSingleAsync(task);
    }
    finally
    {
        semaphore.Release();
    }
}

Full Program Example

class Program
{
    static async Task Main(string[] args)
    {
        var apiKey = Environment.GetEnvironmentVariable("CAPTCHAAI_API_KEY")
            ?? throw new Exception("Set CAPTCHAAI_API_KEY");

        var solver = new BatchSolver(apiKey);

        // Create 20 tasks
        var tasks = new List<CaptchaTask>();
        for (int i = 0; i < 20; i++)
        {
            tasks.Add(new CaptchaTask(
                $"task_{i}",
                "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
                $"https://example.com/page/{i}"
            ));
        }

        Console.WriteLine($"Solving {tasks.Count} CAPTCHAs with concurrency=10...");
        var start = DateTime.UtcNow;

        var result = await solver.SolveWithLimitAsync(tasks, maxConcurrency: 10);

        var elapsed = DateTime.UtcNow - start;
        Console.WriteLine($"\nDone in {elapsed.TotalSeconds:F1}s");
        Console.WriteLine($"  Solved: {result.Solved.Length}");
        Console.WriteLine($"  Failed: {result.Failed.Length}");

        foreach (var s in result.Solved)
            Console.WriteLine($"  ✓ {s.TaskId}: {s.Solution[..Math.Min(30, s.Solution.Length)]}...");

        foreach (var f in result.Failed)
            Console.WriteLine($"  ✗ {f.TaskId}: {f.Error}");
    }
}

Cancellation Support

Cancel all pending tasks after a global timeout:

public async Task<BatchResult> SolveWithTimeoutAsync(
    IReadOnlyList<CaptchaTask> tasks,
    int maxConcurrency = 10,
    TimeSpan? timeout = null)
{
    using var cts = new CancellationTokenSource(
        timeout ?? TimeSpan.FromMinutes(10));

    try
    {
        return await SolveWithLimitAsync(tasks, maxConcurrency);
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Batch operation timed out.");
        return new BatchResult
        {
            Solved = Array.Empty<TaskResult>(),
            Failed = Array.Empty<TaskResult>()
        };
    }
}

Task.WhenAll vs Parallel.ForEachAsync (.NET 6+)

// .NET 6+ alternative
await Parallel.ForEachAsync(tasks,
    new ParallelOptions { MaxDegreeOfParallelism = 10 },
    async (task, ct) =>
    {
        var result = await SolveSingleAsync(task);
        // Process result immediately
    });
Method Collects all results Built-in concurrency limit .NET version
Task.WhenAll + SemaphoreSlim Yes Manual (SemaphoreSlim) .NET Core 1.0+
Parallel.ForEachAsync Process inline Built-in .NET 6+

Troubleshooting

Issue Cause Fix
HttpClient socket exhaustion Creating new HttpClient per request Use single shared HttpClient (shown above)
Task.WhenAll throws on first error Not wrapping individual tasks in try/catch Catch inside SolveSingleAsync (shown above)
High memory with 1000+ tasks All tasks start immediately Use SemaphoreSlim to control concurrency
SSL/TLS errors Old .NET targeting TLS 1.0 Set ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12

FAQ

Should I use a single HttpClient or one per task?

Always a single shared HttpClient. Creating a new one per request causes socket exhaustion. HttpClient is thread-safe and designed for reuse.

What's the optimal concurrency for C#?

Start with 10–20. C#'s async model is lightweight — each concurrent task uses minimal resources. Increase until CaptchaAI's capacity or your network becomes the bottleneck.

Task.WhenAll vs Task.WhenAny?

WhenAll waits for every task. WhenAny returns when the first task completes — useful for "first success wins" scenarios, but not for batch solving where you need all results.

Next Steps

Solve CAPTCHAs concurrently in C# — get your CaptchaAI API key and implement parallel solving.

Related guides:

Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.