Java's CompletableFuture provides a compositional async API — chain submission, polling, and result handling without blocking threads. This tutorial builds a parallel CAPTCHA solving pipeline using CaptchaAI's API.
Prerequisites
<!-- pom.xml — only standard library needed -->
<!-- Java 11+ for HttpClient -->
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<!-- Optional: Gson for JSON parsing -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
CaptchaAI Client
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
public class CaptchaAiClient {
private static final String SUBMIT_URL = "https://ocr.captchaai.com/in.php";
private static final String RESULT_URL = "https://ocr.captchaai.com/res.php";
private final HttpClient httpClient;
private final ScheduledExecutorService scheduler;
private final Gson gson;
private final String apiKey;
public CaptchaAiClient(String apiKey) {
this.apiKey = apiKey;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.build();
this.scheduler = Executors.newScheduledThreadPool(4);
this.gson = new Gson();
}
public CompletableFuture<String> solveCaptcha(String sitekey, String pageurl) {
return submitTask(sitekey, pageurl)
.thenCompose(this::pollResult);
}
private CompletableFuture<String> submitTask(String sitekey, String pageurl) {
String body = buildFormData(Map.of(
"key", apiKey,
"method", "userrecaptcha",
"googlekey", sitekey,
"pageurl", pageurl,
"json", "1"
));
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(SUBMIT_URL))
.header("Content-Type", "application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(resp -> {
JsonObject json = gson.fromJson(resp.body(), JsonObject.class);
if (json.get("status").getAsInt() != 1) {
throw new RuntimeException(
"Submit failed: " + json.get("request").getAsString());
}
return json.get("request").getAsString(); // captcha ID
});
}
private CompletableFuture<String> pollResult(String captchaId) {
CompletableFuture<String> result = new CompletableFuture<>();
scheduler.schedule(
() -> doPoll(captchaId, result, 0),
5, TimeUnit.SECONDS
);
return result;
}
private void doPoll(String captchaId, CompletableFuture<String> result,
int attempt) {
if (attempt >= 60) {
result.completeExceptionally(
new RuntimeException("Timeout after 300s"));
return;
}
String url = String.format(
"%s?key=%s&action=get&id=%s&json=1",
RESULT_URL, apiKey, captchaId
);
httpClient.sendAsync(
HttpRequest.newBuilder().uri(URI.create(url)).GET().build(),
HttpResponse.BodyHandlers.ofString()
).thenAccept(resp -> {
JsonObject json = gson.fromJson(resp.body(), JsonObject.class);
if (json.get("status").getAsInt() == 1) {
result.complete(json.get("request").getAsString());
} else if ("CAPCHA_NOT_READY".equals(
json.get("request").getAsString())) {
// Schedule next poll
scheduler.schedule(
() -> doPoll(captchaId, result, attempt + 1),
5, TimeUnit.SECONDS
);
} else {
result.completeExceptionally(new RuntimeException(
json.get("request").getAsString()));
}
}).exceptionally(ex -> {
result.completeExceptionally(ex);
return null;
});
}
private String buildFormData(Map<String, String> params) {
return params.entrySet().stream()
.map(e -> URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)
+ "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8))
.collect(Collectors.joining("&"));
}
public void shutdown() {
scheduler.shutdown();
}
}
Batch Solving with allOf
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class BatchSolver {
public static void main(String[] args) throws Exception {
String apiKey = System.getenv("CAPTCHAAI_API_KEY");
CaptchaAiClient client = new CaptchaAiClient(apiKey);
// Create tasks
List<CaptchaTask> tasks = new ArrayList<>();
for (int i = 0; i < 20; i++) {
tasks.add(new CaptchaTask(
"task_" + i,
"6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
"https://example.com/page/" + i
));
}
System.out.printf("Solving %d CAPTCHAs...%n", tasks.size());
long start = System.currentTimeMillis();
// Launch all tasks concurrently
List<CompletableFuture<TaskResult>> futures = new ArrayList<>();
for (CaptchaTask task : tasks) {
CompletableFuture<TaskResult> future = client
.solveCaptcha(task.sitekey, task.pageurl)
.thenApply(solution -> new TaskResult(
task.taskId, solution, null))
.exceptionally(ex -> new TaskResult(
task.taskId, null, ex.getMessage()));
futures.add(future);
}
// Wait for all
CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
).join();
// Collect results
long elapsed = System.currentTimeMillis() - start;
int solved = 0, failed = 0;
for (CompletableFuture<TaskResult> f : futures) {
TaskResult result = f.get();
if (result.solution != null) {
solved++;
System.out.printf(" ✓ %s: %s...%n",
result.taskId,
result.solution.substring(0,
Math.min(30, result.solution.length())));
} else {
failed++;
System.out.printf(" ✗ %s: %s%n",
result.taskId, result.error);
}
}
System.out.printf("%nDone in %.1fs — %d solved, %d failed%n",
elapsed / 1000.0, solved, failed);
client.shutdown();
}
}
record CaptchaTask(String taskId, String sitekey, String pageurl) {}
record TaskResult(String taskId, String solution, String error) {}
Concurrency Control with Semaphore
import java.util.concurrent.Semaphore;
public class ThrottledBatchSolver {
private final CaptchaAiClient client;
private final Semaphore semaphore;
public ThrottledBatchSolver(String apiKey, int maxConcurrency) {
this.client = new CaptchaAiClient(apiKey);
this.semaphore = new Semaphore(maxConcurrency);
}
public CompletableFuture<TaskResult> solveThrottled(CaptchaTask task) {
return CompletableFuture.supplyAsync(() -> {
try {
semaphore.acquire();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
return task;
}).thenCompose(t ->
client.solveCaptcha(t.sitekey(), t.pageurl())
.thenApply(sol -> new TaskResult(t.taskId(), sol, null))
.exceptionally(ex -> new TaskResult(t.taskId(), null, ex.getMessage()))
.whenComplete((result, ex) -> semaphore.release())
);
}
}
Chaining Operations
// Submit → Solve → Use token → Report
CompletableFuture<Void> pipeline = client
.solveCaptcha(sitekey, pageurl)
.thenApply(token -> submitForm(token)) // Use the token
.thenApply(response -> extractData(response)) // Process response
.thenAccept(data -> saveToDatabase(data)) // Store result
.exceptionally(ex -> {
System.err.println("Pipeline failed: " + ex.getMessage());
return null;
});
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
CompletionException wrapping real error |
exceptionally wraps in CompletionException |
Call getCause() to get the original exception |
| Thread pool exhaustion | Default ForkJoinPool too small for many tasks | Use custom executor: CompletableFuture.supplyAsync(..., executor) |
| HttpClient connection timeout | Too many concurrent requests | Reduce concurrency with Semaphore; increase HttpClient pool |
| Memory leak from uncompleted futures | Timeout not handled properly | Always set a global timeout on the batch operation |
FAQ
Should I use CompletableFuture or virtual threads (Java 21+)?
For Java 21+, virtual threads simplify concurrent code significantly — you can use blocking Thread.sleep and synchronous HTTP calls in virtual threads. For Java 11–17, CompletableFuture is the standard async approach.
What about Reactive Streams (Project Reactor, RxJava)?
CompletableFuture is sufficient for CAPTCHA solving. Reactive libraries add complexity that's unnecessary unless you're already in a reactive framework (Spring WebFlux).
How many concurrent CAPTCHAs can Java handle?
Java's async model is very efficient. With CompletableFuture, you can manage hundreds of concurrent solves without issues. The limit is CaptchaAI's capacity, not the JVM.
Next Steps
Build async CAPTCHA pipelines in Java — get your CaptchaAI API key and compose your solving workflow.
Related guides:
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.