Tutorials

Node.js Worker Threads for Parallel CAPTCHA Solving

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.

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.