Node.js Worker Threads enable true parallelism — each thread has its own event loop and V8 instance. For CAPTCHA solving at scale, worker threads handle the network I/O and response parsing in parallel without blocking the main thread.
When to use Worker Threads
| Use case | Use Worker Threads? |
|---|---|
| Simple sequential solving | No — use async/await |
| 5-10 concurrent solves | No — use Promise.allSettled |
| Heavy HTML parsing + solving | Yes — offload parsing |
| 50+ concurrent solves | Yes — multiple event loops |
| CPU-intensive image processing | Yes — bypasses main thread |
| Express API server + solving | Yes — keep API responsive |
Basic worker thread solver
Main thread (main.js)
const { Worker } = require("worker_threads");
const path = require("path");
function solveInWorker(method, params) {
return new Promise((resolve, reject) => {
const worker = new Worker(path.join(__dirname, "solver-worker.js"), {
workerData: {
apiKey: process.env.CAPTCHAAI_KEY || "YOUR_API_KEY",
method,
params,
},
});
worker.on("message", (result) => {
if (result.error) {
reject(new Error(result.error));
} else {
resolve(result.token);
}
});
worker.on("error", reject);
worker.on("exit", (code) => {
if (code !== 0) reject(new Error(`Worker exited with code ${code}`));
});
});
}
// Usage
async function main() {
const token = await solveInWorker("userrecaptcha", {
googlekey: "SITEKEY",
pageurl: "https://example.com",
});
console.log(`Token: ${token.substring(0, 50)}...`);
}
main().catch(console.error);
Worker thread (solver-worker.js)
const { parentPort, workerData } = require("worker_threads");
async function solve() {
const { apiKey, method, params } = workerData;
// Submit
const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body: new URLSearchParams({
key: apiKey,
method,
json: "1",
...params,
}),
});
const submitData = await submitResp.json();
if (submitData.status !== 1) {
parentPort.postMessage({ error: submitData.request });
return;
}
const taskId = submitData.request;
// Poll
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: apiKey,
action: "get",
id: taskId,
json: "1",
})}`
);
const data = await pollResp.json();
if (data.status === 1) {
parentPort.postMessage({ token: data.request });
return;
}
if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") {
parentPort.postMessage({ error: "CAPTCHA unsolvable" });
return;
}
}
parentPort.postMessage({ error: "Timed out" });
}
solve().catch((err) => {
parentPort.postMessage({ error: err.message });
});
Worker pool for batch solving
Reuse workers instead of creating new ones per task:
Pool implementation (worker-pool.js)
const { Worker } = require("worker_threads");
const path = require("path");
const { EventEmitter } = require("events");
class WorkerPool extends EventEmitter {
#workers = [];
#available = [];
#queue = [];
#workerPath;
constructor(workerPath, poolSize = 4) {
super();
this.#workerPath = workerPath;
for (let i = 0; i < poolSize; i++) {
this.#addWorker();
}
}
#addWorker() {
const worker = new Worker(this.#workerPath);
this.#workers.push(worker);
this.#available.push(worker);
}
async execute(data) {
const worker = await this.#getWorker();
return new Promise((resolve, reject) => {
const handler = (result) => {
worker.removeListener("error", errorHandler);
this.#available.push(worker);
this.#processQueue();
if (result.error) {
reject(new Error(result.error));
} else {
resolve(result);
}
};
const errorHandler = (err) => {
worker.removeListener("message", handler);
reject(err);
// Replace dead worker
const idx = this.#workers.indexOf(worker);
if (idx >= 0) {
this.#workers.splice(idx, 1);
this.#addWorker();
}
this.#processQueue();
};
worker.once("message", handler);
worker.once("error", errorHandler);
worker.postMessage(data);
});
}
#getWorker() {
if (this.#available.length > 0) {
return Promise.resolve(this.#available.shift());
}
return new Promise((resolve) => {
this.#queue.push(resolve);
});
}
#processQueue() {
if (this.#queue.length > 0 && this.#available.length > 0) {
const resolve = this.#queue.shift();
resolve(this.#available.shift());
}
}
async close() {
await Promise.all(this.#workers.map((w) => w.terminate()));
}
get stats() {
return {
total: this.#workers.length,
available: this.#available.length,
busy: this.#workers.length - this.#available.length,
queued: this.#queue.length,
};
}
}
module.exports = { WorkerPool };
Pool worker (pool-solver-worker.js)
const { parentPort } = require("worker_threads");
parentPort.on("message", async (data) => {
const { apiKey, method, params } = data;
try {
// Submit
const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body: new URLSearchParams({
key: apiKey,
method,
json: "1",
...params,
}),
});
const submitData = await submitResp.json();
if (submitData.status !== 1) {
parentPort.postMessage({ error: submitData.request });
return;
}
const taskId = submitData.request;
// Poll
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 5000));
const pollResp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: apiKey,
action: "get",
id: taskId,
json: "1",
})}`
);
const pollData = await pollResp.json();
if (pollData.status === 1) {
parentPort.postMessage({ token: pollData.request });
return;
}
if (pollData.request === "ERROR_CAPTCHA_UNSOLVABLE") {
parentPort.postMessage({ error: "Unsolvable" });
return;
}
}
parentPort.postMessage({ error: "Timed out" });
} catch (err) {
parentPort.postMessage({ error: err.message });
}
});
Using the pool
const { WorkerPool } = require("./worker-pool");
const path = require("path");
async function main() {
const pool = new WorkerPool(
path.join(__dirname, "pool-solver-worker.js"),
4 // 4 worker threads
);
const API_KEY = "YOUR_API_KEY";
// Solve 20 CAPTCHAs with 4 workers
const tasks = Array.from({ length: 20 }, (_, i) => ({
apiKey: API_KEY,
method: "userrecaptcha",
params: { googlekey: `KEY_${i}`, pageurl: `https://example.com/${i}` },
}));
const results = await Promise.allSettled(
tasks.map((task) => pool.execute(task))
);
const solved = results.filter((r) => r.status === "fulfilled");
console.log(`Solved: ${solved.length}/${results.length}`);
console.log("Pool stats:", pool.stats);
await pool.close();
}
main();
SharedArrayBuffer for progress tracking
Share progress data across threads without message passing overhead:
const { Worker, isMainThread, workerData } = require("worker_threads");
if (isMainThread) {
// Main thread: create shared buffer
const buffer = new SharedArrayBuffer(16); // 4 x Int32
const progress = new Int32Array(buffer);
// progress[0] = submitted, progress[1] = solving, progress[2] = solved, progress[3] = failed
const worker = new Worker(__filename, {
workerData: { buffer, apiKey: "YOUR_API_KEY", tasks: [/* ... */] },
});
// Monitor progress from main thread
const interval = setInterval(() => {
console.log(
`Submitted: ${Atomics.load(progress, 0)}, ` +
`Solving: ${Atomics.load(progress, 1)}, ` +
`Solved: ${Atomics.load(progress, 2)}, ` +
`Failed: ${Atomics.load(progress, 3)}`
);
}, 2000);
worker.on("exit", () => clearInterval(interval));
} else {
// Worker thread: update shared buffer
const progress = new Int32Array(workerData.buffer);
async function solveTask(task) {
Atomics.add(progress, 0, 1); // submitted++
Atomics.add(progress, 1, 1); // solving++
try {
// ... solve CAPTCHA ...
Atomics.add(progress, 2, 1); // solved++
} catch {
Atomics.add(progress, 3, 1); // failed++
} finally {
Atomics.sub(progress, 1, 1); // solving--
}
}
}
Express integration with worker pool
Keep your Express API responsive while solving CAPTCHAs in background threads:
const express = require("express");
const { WorkerPool } = require("./worker-pool");
const path = require("path");
const app = express();
app.use(express.json());
const pool = new WorkerPool(
path.join(__dirname, "pool-solver-worker.js"),
4
);
app.post("/solve", async (req, res) => {
const { method, sitekey, pageurl } = req.body;
try {
const result = await pool.execute({
apiKey: process.env.CAPTCHAAI_KEY,
method,
params: {
[method === "userrecaptcha" ? "googlekey" : "sitekey"]: sitekey,
pageurl,
},
});
res.json({ status: "solved", token: result.token });
} catch (error) {
res.status(500).json({ status: "error", error: error.message });
}
});
app.get("/stats", (req, res) => {
res.json(pool.stats);
});
app.listen(3000, () => console.log("Server on :3000"));
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Cannot use import in worker |
Worker needs CommonJS | Use require() in workers |
| Worker crashes silently | Unhandled promise rejection | Add try/catch in worker |
| Messages not received | Worker exited before sending | Check exit event code |
SharedArrayBuffer not available |
Requires specific flags | Use --experimental-shared-arraybuffer |
| Pool runs out of workers | Workers dying on errors | Add error recovery in pool |
Frequently asked questions
How many worker threads should I use?
For I/O-bound CAPTCHA solving, 2-4 threads is optimal. Each thread can handle multiple concurrent fetches via its own event loop.
Are worker threads faster than async/await?
Not for pure API calls. Worker threads help when you also do CPU-heavy work (HTML parsing, image processing) alongside solving.
Can I share a fetch connection across threads?
No. Each thread has its own network stack. This is actually beneficial — each thread can make independent connections.
Summary
Node.js Worker Threads + CaptchaAI enables true parallel CAPTCHA solving across multiple event loops. Use the WorkerPool pattern for managed thread reuse, SharedArrayBuffer for zero-copy progress tracking, and Express integration for responsive API servers.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.