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.
Related Articles
Next Steps
Solve CAPTCHAs concurrently in C# — get your CaptchaAI API key and implement parallel solving.
Related guides:
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.