Bir CAPTCHA'yı ilk denemede çözmek kolaydır; asıl mühendislik, o çözümü ölçekte ayakta tutmaktır. Üretimde istekler zaman aşımına uğrar, token'ların süresi dolar, bazı hatalar yeniden denemeye hiç değmez. Bu rehber, Node.js ve CaptchaAI ile dayanıklı bir çözüm katmanını beş yapı taşı üzerine kurar:
- Hata sınıflandırması — hangi hataların yeniden denenebilir, hangilerinin ölümcül olduğunu ayırma.
- Üstel geri çekilme — jitter'lı yeniden deneme ile API'yi yormadan toparlanma.
- Devre kesici — arızalı API'yi kısa süreliğine devre dışı bırakma.
- Token önbelleği — süresi dolan token'ları yönetme ve yeniden kullanma.
- Ölçüm — üretimde başarı oranını ve verimi görünür kılma.
Somut bir senaryo: Türkiye'deki bir e-ticaret ekibi ödeme akışını her gece QA botlarıyla test ediyor ve art arda yüzlerce reCAPTCHA çözülüyor. Tek bir geçici hata düzgün ele alınmazsa tüm gece koşusu düşer; CaptchaAI thread bazlı faturalandığı için asıl maliyet de bu boşa harcanan süredir.
Hataları ikiye ayırın: yeniden denenebilir mi, ölümcül mü?
Sağlam bir istemcinin ilk kararı, bir hatanın tekrar denemeye değip değmeyeceğidir. ERROR_ZERO_BALANCE veya yanlış anahtar gibi ölümcül hatalar yalnızca zaman kaybettirir; slot dolu ya da sonuç hazır değilse beklemek mantıklıdır. İki kümeyi özel hata sınıflarıyla modelleyin:
const RETRIABLE_ERRORS = new Set([
"ERROR_NO_SLOT_AVAILABLE",
"CAPCHA_NOT_READY",
]);
const FATAL_ERRORS = new Set([
"ERROR_WRONG_USER_KEY",
"ERROR_KEY_DOES_NOT_EXIST",
"ERROR_ZERO_BALANCE",
"ERROR_CAPTCHA_UNSOLVABLE",
"ERROR_BAD_DUPLICATES",
"ERROR_BAD_PARAMETERS",
"ERROR_WRONG_CAPTCHA_ID",
]);
class CaptchaError extends Error {
constructor(code, message) {
super(message || code);
this.name = "CaptchaError";
this.code = code;
}
}
class RetriableError extends CaptchaError {
constructor(code) {
super(code, `Retriable: ${code}`);
this.name = "RetriableError";
}
}
class FatalError extends CaptchaError {
constructor(code) {
super(code, `Fatal: ${code}`);
this.name = "FatalError";
}
}
function classifyError(code) {
if (FATAL_ERRORS.has(code)) throw new FatalError(code);
throw new RetriableError(code);
}
Jitter'lı üstel geri çekilme (exponential backoff)
Yeniden denenebilir bir hatada hemen tekrar denemeyin. Bekleme süresini her denemede ikiye katlayan üstel geri çekilme (exponential backoff) API baskısını azaltır; rastgele bir sapma (jitter) ise istemcilerin tek noktada yığılmasını (thundering herd) engeller. maxDelay ile gecikmeye tavan koyun:
function sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function withRetry(fn, options = {}) {
const {
maxRetries = 3,
baseDelay = 2000,
maxDelay = 30000,
jitter = true,
} = options;
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (error instanceof FatalError) throw error;
lastError = error;
if (attempt < maxRetries) {
let delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
if (jitter) delay *= 0.5 + Math.random();
console.log(
`Retry ${attempt + 1}/${maxRetries} in ${(delay / 1000).toFixed(1)}s: ${error.message}`
);
await sleep(delay);
}
}
}
throw lastError;
}
Yeniden deneme mantığını içeren sağlam bir çözücü
Şimdi görevi gönderip sonucu sorgulayan (polling) sınıfı kuralım. #submit, in.php uç noktasına POST atar; slot dolu döndüğünde tekrar dener, ölümcül hatada anında durur. #poll, res.php üzerinden CAPCHA_NOT_READY geldikçe sorgulamayı sürdürür, maxPollTime dolunca zaman aşımıyla biter:
const API_KEY = "YOUR_API_KEY";
class RobustSolver {
#apiKey;
#maxRetries;
#pollInterval;
#maxPollTime;
constructor(apiKey, options = {}) {
this.#apiKey = apiKey;
this.#maxRetries = options.maxRetries ?? 3;
this.#pollInterval = options.pollInterval ?? 5000;
this.#maxPollTime = options.maxPollTime ?? 150000;
}
async solve(method, params) {
return withRetry(
() => this.#doSolve(method, params),
{ maxRetries: this.#maxRetries }
);
}
async #doSolve(method, params) {
const taskId = await this.#submit(method, params);
return await this.#poll(taskId);
}
async #submit(method, params) {
for (let attempt = 0; attempt <= this.#maxRetries; attempt++) {
try {
const resp = await fetch("https://ocr.captchaai.com/in.php", {
method: "POST",
body: new URLSearchParams({
key: this.#apiKey,
method,
json: "1",
...params,
}),
signal: AbortSignal.timeout(30000),
});
if (!resp.ok) {
throw new RetriableError(`HTTP_${resp.status}`);
}
const data = await resp.json();
if (data.status === 1) return data.request;
if (data.request === "ERROR_NO_SLOT_AVAILABLE") {
if (attempt < this.#maxRetries) {
await sleep(3000 * (attempt + 1));
continue;
}
}
classifyError(data.request);
} catch (error) {
if (error instanceof FatalError) throw error;
if (error.name === "TimeoutError" || error.name === "AbortError") {
if (attempt < this.#maxRetries) {
await sleep(2000 * (attempt + 1));
continue;
}
}
throw error;
}
}
throw new RetriableError("MAX_SUBMIT_RETRIES");
}
async #poll(taskId) {
const start = Date.now();
while (Date.now() - start < this.#maxPollTime) {
await sleep(this.#pollInterval);
try {
const resp = await fetch(
`https://ocr.captchaai.com/res.php?${new URLSearchParams({
key: this.#apiKey,
action: "get",
id: taskId,
json: "1",
})}`,
{ signal: AbortSignal.timeout(30000) }
);
const data = await resp.json();
if (data.status === 1) return data.request;
if (data.request === "CAPCHA_NOT_READY") continue;
if (FATAL_ERRORS.has(data.request)) throw new FatalError(data.request);
} catch (error) {
if (error instanceof FatalError) throw error;
// Network errors during poll — keep trying
continue;
}
}
throw new CaptchaError("TIMEOUT", `Timed out after ${this.#maxPollTime}ms`);
}
}
Çözüm yaşam döngüsü
Tek bir solve çağrısı şu adımları izler:
- Görevi
in.phpuç noktasına gönderin ve görev kimliğini alın. res.phpüzerinden sonucu belirli aralıklarla sorgulayın.- Token hazır olduğunda döndürün; ölümcül bir hata gelirse hemen durun.
Devre kesici (circuit breaker) ile arızalı API'yi izole edin
API gerçekten kapandığında her isteği tek tek denemek yalnızca gecikme biriktirir. Devre kesici üç durum arasında geçiş yapar:
- closed — istekler normal akar; arızalar sayılır.
- open — eşik aşıldı;
resetTimeoutboyunca istekler hızlıca reddedilir. - half-open — tek bir test isteğiyle servisin toparlanıp toparlanmadığı yoklanır; başarılıysa devre kapanır, değilse yeniden açılır.
Bu geçişleri tek bir sarmalayıcıda toplayın:
class CircuitBreaker {
#state = "closed"; // closed | open | half-open
#failures = 0;
#lastFailure = 0;
#threshold;
#resetTimeout;
constructor(threshold = 5, resetTimeout = 60000) {
this.#threshold = threshold;
this.#resetTimeout = resetTimeout;
}
get state() {
return this.#state;
}
canExecute() {
if (this.#state === "closed") return true;
if (this.#state === "open") {
if (Date.now() - this.#lastFailure > this.#resetTimeout) {
this.#state = "half-open";
return true;
}
return false;
}
return true; // half-open: allow test request
}
recordSuccess() {
this.#failures = 0;
this.#state = "closed";
}
recordFailure() {
this.#failures++;
this.#lastFailure = Date.now();
if (this.#failures >= this.#threshold) {
this.#state = "open";
console.log(`Circuit OPEN — pausing for ${this.#resetTimeout / 1000}s`);
}
}
}
class ProtectedSolver {
#solver;
#breaker;
constructor(apiKey) {
this.#solver = new RobustSolver(apiKey);
this.#breaker = new CircuitBreaker(5, 60000);
}
async solve(method, params) {
if (!this.#breaker.canExecute()) {
throw new CaptchaError(
"CIRCUIT_OPEN",
"API appears down — circuit breaker is open"
);
}
try {
const result = await this.#solver.solve(method, params);
this.#breaker.recordSuccess();
return result;
} catch (error) {
if (error instanceof FatalError) throw error;
this.#breaker.recordFailure();
throw error;
}
}
get circuitState() {
return this.#breaker.state;
}
}
Devre kesiciyi ne zaman kullanmalı
Devre kesici, tek tük geçici hataları değil sürekli kesintileri hedefler. Sağlıklı bir API'de neredeyse hiç tetiklenmez; asıl değeri, servis gerçekten düştüğünde çağıran kodu boş yere bekletmemesidir.
Token süre sonu ve önbellekleme
Çözülen token'ın ömrü sınırlıdır — reCAPTCHA için yaklaşık 2 dakika, Turnstile için yaklaşık 5 dakika. TokenCache, token'ları TTL süresince saklayıp süresi dolanları otomatik atar; solveWithRetryOnReject ise reddedilen token'ı bir üst sınıra kadar yeniden çözer:
class TokenCache {
#cache = new Map();
#defaultTTL;
constructor(defaultTTL = 110000) {
// reCAPTCHA: ~2 min, Turnstile: ~5 min
this.#defaultTTL = defaultTTL;
}
get(key) {
const entry = this.#cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > this.#defaultTTL) {
this.#cache.delete(key);
return null;
}
return entry.token;
}
set(key, token) {
this.#cache.set(key, { token, timestamp: Date.now() });
}
invalidate(key) {
this.#cache.delete(key);
}
}
class CachedSolver {
#solver;
#cache;
constructor(apiKey) {
this.#solver = new ProtectedSolver(apiKey);
this.#cache = new TokenCache(110000);
}
async getToken(cacheKey, method, params) {
const cached = this.#cache.get(cacheKey);
if (cached) return cached;
const token = await this.#solver.solve(method, params);
this.#cache.set(cacheKey, token);
return token;
}
async solveWithRetryOnReject(method, params, submitFn, maxAttempts = 2) {
for (let i = 0; i < maxAttempts; i++) {
const token = await this.#solver.solve(method, params);
const accepted = await submitFn(token);
if (accepted) return token;
console.log(`Token rejected (attempt ${i + 1}), re-solving...`);
}
throw new CaptchaError("TOKEN_REJECTED", "Token rejected after max attempts");
}
}
Önbellekleme ne zaman güvenli
Aynı sayfa veya anahtar için kısa ömürlü token'ları önbelleğe almak istek sayısını azaltır. TTL değerini token'ın gerçek ömrünün altında tutun ki süresi dolmuş bir token'ı asla yeniden kullanmayasınız.
Loglama ve ölçümler
Üretimde neyin yavaşladığını ancak ölçerek anlarsınız. En azından şu sinyalleri toplayın:
- Başarı oranı — düşüş, yapılandırma ya da servis sorununun erken habercisidir.
- Ortalama çözüm süresi — token'ın süre sonu bütçenizi belirler.
- Dakikalık verim (throughput) — thread ayırmanızın yeterli olup olmadığını gösterir.
SolverMetrics bu değerleri tutar; InstrumentedSolver ise sayaçları her çağrıda günceller:
class SolverMetrics {
#startTime = Date.now();
#solveTimes = [];
#counts = { submitted: 0, solved: 0, failed: 0, retries: 0 };
recordSubmit() { this.#counts.submitted++; }
recordSolved(duration) { this.#counts.solved++; this.#solveTimes.push(duration); }
recordFailed() { this.#counts.failed++; }
recordRetry() { this.#counts.retries++; }
report() {
const elapsed = (Date.now() - this.#startTime) / 1000;
const total = this.#counts.solved + this.#counts.failed;
const avgTime = this.#solveTimes.length > 0
? this.#solveTimes.reduce((a, b) => a + b, 0) / this.#solveTimes.length / 1000
: 0;
return {
elapsed: `${elapsed.toFixed(0)}s`,
submitted: this.#counts.submitted,
solved: this.#counts.solved,
failed: this.#counts.failed,
retries: this.#counts.retries,
avgSolveTime: `${avgTime.toFixed(1)}s`,
successRate: total > 0 ? `${((this.#counts.solved / total) * 100).toFixed(1)}%` : "N/A",
throughput: `${(this.#counts.solved / (elapsed / 60)).toFixed(1)}/min`,
};
}
}
class InstrumentedSolver {
#solver;
#metrics;
constructor(apiKey) {
this.#solver = new ProtectedSolver(apiKey);
this.#metrics = new SolverMetrics();
}
async solve(method, params) {
this.#metrics.recordSubmit();
const start = Date.now();
try {
const token = await this.#solver.solve(method, params);
this.#metrics.recordSolved(Date.now() - start);
return token;
} catch (error) {
this.#metrics.recordFailed();
throw error;
}
}
report() {
return this.#metrics.report();
}
}
Her şeyi birleştiren üretim deseni
Beş katman birleşince tek bir solve çağrısı yeniden deneme, geri çekilme, devre kesici ve ölçümü kendiliğinden içerir. Promise.allSettled ile onlarca görevi paralel çalıştırır, başarısızlar tüm partiyi düşürmeden sonuçları ayrı toplarsınız:
// Combine everything
const solver = new InstrumentedSolver("YOUR_API_KEY");
async function main() {
const tasks = Array.from({ length: 10 }, (_, i) => ({
method: "userrecaptcha",
params: { googlekey: `KEY_${i}`, pageurl: `https://example.com/${i}` },
}));
const results = await Promise.allSettled(
tasks.map((task) => solver.solve(task.method, task.params))
);
const solved = results.filter((r) => r.status === "fulfilled");
const failed = results.filter((r) => r.status === "rejected");
console.log(`Solved: ${solved.length}, Failed: ${failed.length}`);
console.log("Metrics:", solver.report());
for (const fail of failed) {
console.log(` Error: ${fail.reason.message}`);
}
}
main();
İpucu: Token'ı hedef sayfaya gitmeden hemen önce çözün. Çözüm ile kullanım arasındaki süre ne kadar kısaysa, token'ın süresi dolduğu için reddedilme olasılığı o kadar düşer.
Sorun giderme
| Belirti | Sebep | Çözüm |
|---|---|---|
| Tüm yeniden denemeler anında başarısız oluyor | Ölümcül bir hata boşuna tekrar deneniyor | Hata sınıflandırmasını gözden geçirin |
| Devre kesici açık kalıyor | API kapalı ya da API anahtarı yanlış | API durumunu ve anahtarını doğrulayın |
| Token gönderim anında süresi dolmuş oluyor | Çözüm süresi + gecikme fazla uzun | Sayfaya gitmeden önce token'ı önceden çözün |
İstekte AbortError alıyorsunuz |
Zaman aşımı çok kısa ayarlanmış | AbortSignal.timeout değerini artırın |
UnhandledPromiseRejection uyarısı |
Async çağrıda catch eksik |
Reddedilen promise'leri her zaman yakalayın |
Sık sorulan sorular
Yeniden denemeler CaptchaAI bakiyemi hızla tüketir mi?
Hayır. CaptchaAI thread bazlı faturalanır ve her plan thread başına sınırsız çözüm içerir; yeniden denemeler için ayrı "çözüm başına" ücret yoktur. Maliyeti, BASIC ($15/ay, 5 thread) gibi planlardaki eşzamanlı thread sayınız belirler.
Üstel geri çekilmeye jitter'ı neden ekliyoruz?
Sabit aralıklı yeniden denemeler birçok istemcinin aynı anda tekrar denemesine (thundering herd) yol açar. Rastgele sapma denemeleri zamana yayar ve toparlanan API'yi ikinci bir dalgayla devirmenizi önler.
Devre kesici eşiğini kaça ayarlamalıyım?
Başlangıç için 5 ardışık arıza ve 60 saniyelik resetTimeout çoğu iş yükü için makuldür. Yüksek hacimli koşularda eşiği biraz yükseltip toparlanma süresini kısaltabilirsiniz; asıl amaç, tek tük geçici hatalarda değil gerçek bir kesintide devreyi açmaktır. Devreyi çok erken açan bir yapılandırma, sağlıklı bir API'yi gereksiz yere devre dışı bırakır.
Ölümcül bir hatayı yeniden denenebilir olandan nasıl ayırırım?
Ölümcül hatalar tekrar denemekle düzelmez: yanlış anahtar, boş bakiye, çözülemeyen CAPTCHA veya hatalı parametre. Slot doluluğu ya da "sonuç hazır değil" gibi durumlar ise geçicidir ve yeniden denenebilir. Kod tarafında da iki katmanı ayırın: ağ hataları (AbortError, TypeError) ile API'nin JSON içinde döndürdüğü hata kodları farklı yollarla ele alınmalıdır.
Zaman aşımı ve sorgulama aralığını nasıl seçmeliyim?
Sorgulama aralığını çok kısa tutmak API'yi gereksiz istekle yorar, çok uzun tutmak ise token'ın ömrünü boşa harcar. reCAPTCHA gibi türlerde 5 saniyelik pollInterval ve toplam 150 saniyelik maxPollTime dengeli bir başlangıçtır; ölçtüğünüz gerçek çözüm sürelerine göre ayarlayın.
Özet
Node.js'te dayanıklı CAPTCHA çözümü beş desende toplanır: hataları yeniden denenebilir ve ölümcül olarak ayırın, jitter'lı üstel geri çekilme uygulayın, arızalı API'yi devre kesiciyle izole edin, token'ları önbelleğe alın ve her şeyi ölçümlerle görünür kılın. Bu katmanlar CaptchaAI entegrasyonunuzu üretim kalitesine taşır.