Tutorials

Building a CAPTCHA Solving Queue in Node.js

Node.js is single-threaded but excels at I/O concurrency — perfect for CAPTCHA solving where you're waiting on API responses. This guide covers queue patterns from simple Promise.all to production-grade job systems.


Simple batch: Promise.allSettled

const API_KEY = "YOUR_API_KEY";

function sleep(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function solveSingle(method, params) {
  const submitResp = await fetch("https://ocr.captchaai.com/in.php", {
    method: "POST",
    body: new URLSearchParams({ key: API_KEY, method, json: "1", ...params }),
  });
  const submitData = await submitResp.json();
  if (submitData.status !== 1) throw new Error(submitData.request);
  const taskId = submitData.request;

  for (let i = 0; i < 30; i++) {
    await sleep(5000);
    const pollResp = await fetch(
      `https://ocr.captchaai.com/res.php?${new URLSearchParams({
        key: API_KEY,
        action: "get",
        id: taskId,
        json: "1",
      })}`
    );
    const data = await pollResp.json();
    if (data.status === 1) return data.request;
    if (data.request === "ERROR_CAPTCHA_UNSOLVABLE") throw new Error("Unsolvable");
  }
  throw new Error("Timed out");
}

// Solve all at once
async function solveBatch(tasks) {
  const results = await Promise.allSettled(
    tasks.map((task) => solveSingle(task.method, task.params))
  );

  return results.map((result, i) => ({
    taskId: tasks[i].id,
    status: result.status,
    value: result.status === "fulfilled" ? result.value : null,
    error: result.status === "rejected" ? result.reason.message : null,
  }));
}

// Usage
const tasks = Array.from({ length: 10 }, (_, i) => ({
  id: i,
  method: "userrecaptcha",
  params: { googlekey: `KEY_${i}`, pageurl: `https://example.com/${i}` },
}));

const results = await solveBatch(tasks);
console.log(`Solved: ${results.filter((r) => r.status === "fulfilled").length}/10`);

Concurrency-limited queue

Control how many CAPTCHA solves run in parallel:

class ConcurrencyQueue {
  constructor(maxConcurrent = 5) {
    this.maxConcurrent = maxConcurrent;
    this.running = 0;
    this.queue = [];
    this.results = [];
  }

  add(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ fn, resolve, reject });
      this.#process();
    });
  }

  async #process() {
    if (this.running >= this.maxConcurrent || this.queue.length === 0) return;

    this.running++;
    const { fn, resolve, reject } = this.queue.shift();

    try {
      const result = await fn();
      resolve(result);
    } catch (error) {
      reject(error);
    } finally {
      this.running--;
      this.#process();
    }
  }

  async addBatch(fns) {
    return Promise.allSettled(fns.map((fn) => this.add(fn)));
  }
}

// Usage
const queue = new ConcurrencyQueue(5);

const tasks = Array.from({ length: 20 }, (_, i) => () =>
  solveSingle("userrecaptcha", {
    googlekey: `KEY_${i}`,
    pageurl: `https://example.com/${i}`,
  })
);

const results = await queue.addBatch(tasks);
const solved = results.filter((r) => r.status === "fulfilled");
console.log(`Solved: ${solved.length}/${results.length}`);

EventEmitter-based queue

For real-time progress tracking:

const { EventEmitter } = require("events");

class CaptchaQueue extends EventEmitter {
  #apiKey;
  #maxConcurrent;
  #pending;
  #active;

  constructor(apiKey, maxConcurrent = 5) {
    super();
    this.#apiKey = apiKey;
    this.#maxConcurrent = maxConcurrent;
    this.#pending = [];
    this.#active = 0;
    this.stats = { submitted: 0, solved: 0, failed: 0 };
  }

  submit(id, method, params) {
    this.#pending.push({ id, method, params });
    this.stats.submitted++;
    this.emit("submitted", { id, total: this.stats.submitted });
    this.#drain();
  }

  async #drain() {
    while (this.#active < this.#maxConcurrent && this.#pending.length > 0) {
      const task = this.#pending.shift();
      this.#active++;
      this.#solve(task).finally(() => {
        this.#active--;
        this.#drain();
        if (this.#active === 0 && this.#pending.length === 0) {
          this.emit("complete", this.stats);
        }
      });
    }
  }

  async #solve(task) {
    try {
      const token = await solveSingle(task.method, task.params);
      this.stats.solved++;
      this.emit("solved", { id: task.id, token, stats: { ...this.stats } });
    } catch (error) {
      this.stats.failed++;
      this.emit("failed", { id: task.id, error: error.message, stats: { ...this.stats } });
    }
  }
}

// Usage
const queue = new CaptchaQueue("YOUR_API_KEY", 5);

queue.on("submitted", ({ id, total }) => {
  console.log(`Submitted #${id} (total: ${total})`);
});

queue.on("solved", ({ id, stats }) => {
  console.log(`Solved #${id} — ${stats.solved}/${stats.submitted}`);
});

queue.on("failed", ({ id, error }) => {
  console.log(`Failed #${id}: ${error}`);
});

queue.on("complete", (stats) => {
  const rate = ((stats.solved / stats.submitted) * 100).toFixed(1);
  console.log(`Done: ${stats.solved}/${stats.submitted} (${rate}%)`);
});

// Submit tasks
for (let i = 0; i < 15; i++) {
  queue.submit(i, "userrecaptcha", {
    googlekey: `KEY_${i}`,
    pageurl: `https://example.com/${i}`,
  });
}

Priority queue

class PriorityQueue {
  #items = [];

  enqueue(item, priority) {
    this.#items.push({ item, priority });
    this.#items.sort((a, b) => a.priority - b.priority);
  }

  dequeue() {
    return this.#items.shift()?.item;
  }

  get length() {
    return this.#items.length;
  }
}

class PriorityCaptchaQueue {
  #apiKey;
  #maxConcurrent;
  #queue;
  #active;
  #results;

  constructor(apiKey, maxConcurrent = 5) {
    this.#apiKey = apiKey;
    this.#maxConcurrent = maxConcurrent;
    this.#queue = new PriorityQueue();
    this.#active = 0;
    this.#results = new Map();
  }

  submit(id, method, params, priority = 5) {
    return new Promise((resolve, reject) => {
      this.#queue.enqueue({ id, method, params, resolve, reject }, priority);
      this.#drain();
    });
  }

  async #drain() {
    while (this.#active < this.#maxConcurrent && this.#queue.length > 0) {
      const task = this.#queue.dequeue();
      this.#active++;

      solveSingle(task.method, task.params)
        .then((token) => {
          this.#results.set(task.id, { status: "solved", token });
          task.resolve(token);
        })
        .catch((err) => {
          this.#results.set(task.id, { status: "error", error: err.message });
          task.reject(err);
        })
        .finally(() => {
          this.#active--;
          this.#drain();
        });
    }
  }
}

// Usage: high-priority checkout, low-priority scraping
const pq = new PriorityCaptchaQueue("YOUR_API_KEY", 3);

// Priority 1 (highest) — checkout
const checkoutToken = pq.submit(
  "checkout_1",
  "turnstile",
  { sitekey: "KEY", pageurl: "https://shop.com/checkout" },
  1
);

// Priority 5 (normal) — product scraping
for (let i = 0; i < 5; i++) {
  pq.submit(
    `product_${i}`,
    "userrecaptcha",
    { googlekey: "KEY", pageurl: `https://shop.com/p/${i}` },
    5
  );
}

Retry queue with dead-letter handling

class RetryQueue {
  #apiKey;
  #maxRetries;
  #results;
  #deadLetter;

  constructor(apiKey, maxRetries = 3) {
    this.#apiKey = apiKey;
    this.#maxRetries = maxRetries;
    this.#results = [];
    this.#deadLetter = [];
  }

  async processBatch(tasks, maxConcurrent = 5) {
    const queue = tasks.map((t) => ({ ...t, attempts: 0 }));

    while (queue.length > 0) {
      const batch = queue.splice(0, maxConcurrent);
      const results = await Promise.allSettled(
        batch.map((task) => this.#solveWithRetry(task))
      );

      for (let i = 0; i < results.length; i++) {
        const result = results[i];
        const task = batch[i];

        if (result.status === "fulfilled") {
          this.#results.push({ id: task.id, token: result.value });
        } else {
          task.attempts++;
          if (task.attempts < this.#maxRetries) {
            queue.push(task); // Retry
            console.log(`Retry ${task.attempts}/${this.#maxRetries}: ${task.id}`);
          } else {
            this.#deadLetter.push({
              id: task.id,
              error: result.reason.message,
              attempts: task.attempts,
            });
          }
        }
      }
    }

    return {
      solved: this.#results,
      failed: this.#deadLetter,
    };
  }

  async #solveWithRetry(task) {
    return solveSingle(task.method, task.params);
  }
}

Monitoring dashboard

class QueueMonitor {
  #startTime;
  #solveTimes;

  constructor() {
    this.#startTime = Date.now();
    this.#solveTimes = [];
    this.counts = { submitted: 0, solving: 0, solved: 0, failed: 0 };
  }

  recordSubmit() {
    this.counts.submitted++;
    this.counts.solving++;
  }

  recordSolved(solveTime) {
    this.counts.solving--;
    this.counts.solved++;
    this.#solveTimes.push(solveTime);
  }

  recordFailed() {
    this.counts.solving--;
    this.counts.failed++;
  }

  report() {
    const elapsed = (Date.now() - this.#startTime) / 1000;
    const avgTime =
      this.#solveTimes.length > 0
        ? this.#solveTimes.reduce((a, b) => a + b, 0) / this.#solveTimes.length
        : 0;
    const throughput = this.counts.solved / (elapsed / 60);
    const successRate =
      this.counts.solved + this.counts.failed > 0
        ? (this.counts.solved / (this.counts.solved + this.counts.failed)) * 100
        : 0;

    return {
      elapsed: `${elapsed.toFixed(0)}s`,
      submitted: this.counts.submitted,
      solving: this.counts.solving,
      solved: this.counts.solved,
      failed: this.counts.failed,
      avgSolveTime: `${(avgTime / 1000).toFixed(1)}s`,
      throughput: `${throughput.toFixed(1)}/min`,
      successRate: `${successRate.toFixed(1)}%`,
    };
  }
}

Troubleshooting

Symptom Cause Fix
All promises reject simultaneously API rate limit hit Lower maxConcurrent
Memory grows over time Results accumulating Process and clear results periodically
Queue drains but tasks remain Missing drain() call after completion Check drain trigger in finally block
ERROR_NO_SLOT_AVAILABLE Too many concurrent API calls Add delay between submissions
Dead letter queue fills up Persistent errors Check error types — may need parameter fix

Frequently asked questions

How many concurrent solves should I run?

Start with 5-10 and increase based on your CaptchaAI plan. Watch for ERROR_NO_SLOT_AVAILABLE as the throttle signal.

Should I use a library like p-queue or bull?

For simple use cases, the built-in patterns above are sufficient. Use bull or bullmq for persistent Redis-backed queues in production multi-server setups.

How do I handle queue backpressure?

Limit the queue size and reject or delay new submissions when full. The ConcurrencyQueue pattern handles this naturally.


Summary

Node.js excels at concurrent I/O — perfect for CAPTCHA queue systems with CaptchaAI. Use Promise.allSettled for simple batches, EventEmitter for progress tracking, priority queues for business-critical flows, and retry queues for reliability.

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.