Aynı sitekey ve pageurl kombinasyonunu hedefleyen 5 worker aynı anda CAPTCHA gönderirse, teoride 5 ayrı çözüm için ödeme yaparsınız — oysa gereken tek bir sonuçtur. Tekilleştirme (deduplication) katmanı bu çakışan istekleri API'ye ulaşmadan yakalar, ilk çözümü bekleyen tüm worker'lara aynı token'ı döndürür ve BASIC planınızın 5 thread'ini ($15/ay) ya da VIP-3'ün 5.000 thread'ini ($7,500/ay) gereksiz yere doldurmanızı önler. Aşağıda Redis tabanlı ve PostgreSQL tabanlı iki uygulamayı, anahtar tasarımını ve gerçek bir tasarruf hesaplamasını bulacaksınız.
Yinelenen CAPTCHA İstekleri Nereden Geliyor?
Tekilleştirmenin nerede devreye girdiğini görmek için önce israfın kaynağına bakalım:
| Senaryo | Sebep | Maliyet |
|---|---|---|
| Sonuç gelmeden yeniden deneme | Agresif retry mantığı | CAPTCHA başına 2-5 kat maliyet |
| Aynı hedefe birden fazla worker | Worker'lar arasında koordinasyon yok | Paralel çalışan boşa giden çözümler |
| Sayfa yenileme yeniden tetikler | Zaman aşımında frontend retry | Her yenilemede ekstra çözüm |
| Kuyruk mesajı tekrar oynatılır | En az bir kez teslim ilkesi (at-least-once) | Her tekrarda yinelenen çözüm |
Örnek: Bir e-ticaret ekibinin ödeme (checkout) akışını uçtan uca test eden QA otomasyon paketini düşünün. Regresyon testleri staging ortamındaki reCAPTCHA'yı saatte yüzlerce kez tetikliyor ve 6–8 paralel worker bunları çözüyor. Tekilleştirme katmanı olmadan, aynı test senaryosunun art arda çalıştırılan kopyaları da ayrı ayrı çözülür ve CI/CD pipeline'ında gereksiz thread tüketimine yol açar — staging verisi gerçek müşteri PII'si içermediği için bu, KVKK açısından da temiz bir test kurgusudur.
Tekilleştirme Anahtarı Nasıl Tasarlanır?
İstek parametrelerinden benzersiz bir anahtar üretin:
import hashlib
def dedup_key(method, sitekey, pageurl):
"""Generate a deduplication key for a CAPTCHA solve request."""
raw = f"{method}:{sitekey}:{pageurl}"
return f"captcha:dedup:{hashlib.sha256(raw.encode()).hexdigest()[:16]}"
Bu anahtarın üç önemli özelliği var:
- Aynı istek parametreleri her zaman aynı anahtarı üretir (deterministik) — worker'lar arasında koordinasyon gerekmez.
- SHA-256'nın ilk 16 karakteri, gerçek trafik hacminde çakışma riskini pratikte sıfıra indirir.
- Anahtar doğrudan Redis
SET/GETveya PostgreSQL sorgu parametresi olarak kullanılabilir; ayrıca bir kodlama adımı gerekmez.
Anahtar bileşenleri CAPTCHA türüne göre değişir:
| CAPTCHA Türü | Anahtar Bileşenleri |
|---|---|
| reCAPTCHA v2 | method + sitekey + pageurl |
| reCAPTCHA v3 | method + sitekey + pageurl + action |
| hCaptcha | method + sitekey + pageurl |
| Turnstile | method + sitekey + pageurl |
| Resim CAPTCHA'sı | method + body karması (resim içeriği) |
Not: Anahtara proxy bilgisini eklemeyin — çözüm token'ı hangi proxy ile elde edildiğine bakmaksızın geçerlidir. Proxy'yi anahtara dahil etmek tekilleştirmeyi tamamen etkisiz hale getirir.
Redis ile Tekilleştirme Katmanı Kurulumu
Redis, solving/solved/error durumlarını kısa TTL'lerle tutmak için doğal bir seçimdir — aşağıdaki iki uygulama aynı anahtar tasarımını paylaşır.
Python Uygulaması
import os
import time
import json
import hashlib
import redis
import requests
r = redis.Redis(
host=os.environ.get("REDIS_HOST", "localhost"),
port=int(os.environ.get("REDIS_PORT", 6379)),
decode_responses=True
)
API_KEY = os.environ["CAPTCHAAI_API_KEY"]
# Dedup window: how long to consider a request "in progress"
DEDUP_TTL = 180 # seconds
def dedup_key(method, sitekey, pageurl, extra=""):
raw = f"{method}:{sitekey}:{pageurl}:{extra}"
return f"captcha:dedup:{hashlib.sha256(raw.encode()).hexdigest()[:16]}"
def solve_with_dedup(sitekey, pageurl, method="userrecaptcha"):
key = dedup_key(method, sitekey, pageurl)
# Check if this request is already being solved
existing = r.get(key)
if existing:
state = json.loads(existing)
if state["status"] == "solving":
# Wait for the result
return wait_for_result(key)
elif state["status"] == "solved":
return {"solution": state["solution"], "source": "dedup_cache"}
elif state["status"] == "error":
pass # Allow retry on error
# Mark as solving
r.set(key, json.dumps({"status": "solving", "started": time.time()}), ex=DEDUP_TTL)
# Submit to CaptchaAI
resp = requests.post("https://ocr.captchaai.com/in.php", data={
"key": API_KEY,
"method": method,
"googlekey": sitekey,
"pageurl": pageurl,
"json": 1
})
data = resp.json()
if data.get("status") != 1:
r.set(key, json.dumps({"status": "error", "error": data.get("request")}), ex=30)
return {"error": data.get("request")}
captcha_id = data["request"]
# Poll for result
for _ in range(60):
time.sleep(5)
result = requests.get("https://ocr.captchaai.com/res.php", params={
"key": API_KEY, "action": "get",
"id": captcha_id, "json": 1
}).json()
if result.get("status") == 1:
solution = result["request"]
# Cache the result for other workers (short TTL since tokens expire)
r.set(key, json.dumps({
"status": "solved",
"solution": solution,
"solved_at": time.time()
}), ex=60) # Cache result for 60 seconds
return {"solution": solution, "source": "api"}
if result.get("request") != "CAPCHA_NOT_READY":
r.set(key, json.dumps({
"status": "error", "error": result.get("request")
}), ex=30)
return {"error": result.get("request")}
r.set(key, json.dumps({"status": "error", "error": "TIMEOUT"}), ex=30)
return {"error": "TIMEOUT"}
def wait_for_result(key, timeout=120):
"""Wait for another worker to finish solving."""
start = time.time()
while time.time() - start < timeout:
data = r.get(key)
if data:
state = json.loads(data)
if state["status"] == "solved":
return {"solution": state["solution"], "source": "dedup_wait"}
if state["status"] == "error":
return {"error": state.get("error", "UNKNOWN")}
time.sleep(2)
return {"error": "DEDUP_WAIT_TIMEOUT"}
Node.js Uygulaması
const Redis = require("ioredis");
const axios = require("axios");
const crypto = require("crypto");
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");
const API_KEY = process.env.CAPTCHAAI_API_KEY;
const DEDUP_TTL = 180;
function dedupKey(method, sitekey, pageurl) {
const raw = `${method}:${sitekey}:${pageurl}`;
const hash = crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
return `captcha:dedup:${hash}`;
}
async function solveWithDedup(sitekey, pageurl, method = "userrecaptcha") {
const key = dedupKey(method, sitekey, pageurl);
// Check existing
const existing = await redis.get(key);
if (existing) {
const state = JSON.parse(existing);
if (state.status === "solving") return await waitForResult(key);
if (state.status === "solved") return { solution: state.solution, source: "dedup_cache" };
}
// Mark as solving
await redis.set(key, JSON.stringify({ status: "solving", started: Date.now() }), "EX", DEDUP_TTL);
// Submit
const submit = await axios.post("https://ocr.captchaai.com/in.php", null, {
params: { key: API_KEY, method, googlekey: sitekey, pageurl, json: 1 },
});
if (submit.data.status !== 1) {
await redis.set(key, JSON.stringify({ status: "error", error: submit.data.request }), "EX", 30);
return { error: submit.data.request };
}
const captchaId = submit.data.request;
for (let i = 0; i < 60; i++) {
await new Promise((r) => setTimeout(r, 5000));
const poll = await axios.get("https://ocr.captchaai.com/res.php", {
params: { key: API_KEY, action: "get", id: captchaId, json: 1 },
});
if (poll.data.status === 1) {
await redis.set(key, JSON.stringify({ status: "solved", solution: poll.data.request }), "EX", 60);
return { solution: poll.data.request, source: "api" };
}
if (poll.data.request !== "CAPCHA_NOT_READY") {
await redis.set(key, JSON.stringify({ status: "error", error: poll.data.request }), "EX", 30);
return { error: poll.data.request };
}
}
await redis.set(key, JSON.stringify({ status: "error", error: "TIMEOUT" }), "EX", 30);
return { error: "TIMEOUT" };
}
async function waitForResult(key, timeout = 120000) {
const start = Date.now();
while (Date.now() - start < timeout) {
const data = await redis.get(key);
if (data) {
const state = JSON.parse(data);
if (state.status === "solved") return { solution: state.solution, source: "dedup_wait" };
if (state.status === "error") return { error: state.error };
}
await new Promise((r) => setTimeout(r, 2000));
}
return { error: "DEDUP_WAIT_TIMEOUT" };
}
Redis Olmadan: PostgreSQL Advisory Lock ile Tekilleştirme
Redis'i mimarinize eklemek istemiyorsanız, aynı mantığı PostgreSQL'in advisory lock mekanizmasıyla kurabilirsiniz — ekstra bir bağımlılık gerekmez:
import psycopg2
def solve_with_pg_dedup(conn, sitekey, pageurl):
"""Use PostgreSQL advisory locks for deduplication."""
# Generate a numeric lock key from the dedup key
lock_id = hash(f"{sitekey}:{pageurl}") & 0x7FFFFFFF
cursor = conn.cursor()
# Try to acquire advisory lock (non-blocking)
cursor.execute("SELECT pg_try_advisory_lock(%s)", (lock_id,))
acquired = cursor.fetchone()[0]
if not acquired:
# Another worker is solving — wait for result
cursor.execute("SELECT pg_advisory_lock(%s)", (lock_id,))
# Lock acquired means other worker finished — check cache
cursor.execute(
"SELECT solution FROM captcha_cache "
"WHERE sitekey = %s AND pageurl = %s "
"AND created_at > NOW() - INTERVAL '60 seconds'",
(sitekey, pageurl)
)
row = cursor.fetchone()
cursor.execute("SELECT pg_advisory_unlock(%s)", (lock_id,))
if row:
return {"solution": row[0], "source": "pg_cache"}
return {"error": "NO_CACHED_RESULT"}
try:
# Solve the CAPTCHA
solution = solve_via_api(sitekey, pageurl)
if solution:
cursor.execute(
"INSERT INTO captcha_cache (sitekey, pageurl, solution) "
"VALUES (%s, %s, %s)",
(sitekey, pageurl, solution)
)
conn.commit()
return {"solution": solution} if solution else {"error": "SOLVE_FAILED"}
finally:
cursor.execute("SELECT pg_advisory_unlock(%s)", (lock_id,))
Redis mi, PostgreSQL mi?
İkisi de aynı kilitleme mantığını sağlar; seçim mimarinizde zaten ne olduğuna bağlıdır.
Redis Ne Zaman Daha Uygun?
- Worker havuzu zaten queue veya cache için Redis kullanıyorsa — çoğu otomasyon mimarisinde bu zaten böyledir.
- Ekstra taşıma maliyeti istemiyorsanız: anahtar tasarımı ve TTL mantığı yukarıdaki koda birebir uyar.
- 100'den fazla worker gibi yüksek eşzamanlılıkta Redis genellikle daha öngörülebilir gecikme sunar.
PostgreSQL Ne Zaman Daha Uygun?
- State zaten PostgreSQL'de tutuluyorsa ve mimariye yeni bir servis eklemek istemiyorsanız.
pg_advisory_lock, ekstra bir bağımlılık eklemeden aynı karşılıklı dışlama (mutual exclusion) mantığını sağlar.- Tek dezavantajı: kilidi bekleyen worker'ın veritabanı bağlantısını o süre boyunca meşgul tutmasıdır — çok yüksek worker sayısında bağlantı havuzunu zorlayabilir.
Sorun Giderme
Üretimde en sık karşılaşılan dört tekilleştirme sorunu ve hızlı düzeltmeleri:
| Sorun | Sebep | Düzeltme |
|---|---|---|
| Yinelenen anahtar çarpışmaları | Karma değeri çok kısa veya eksik parametreler | CAPTCHA'ya özgü tüm parametreleri anahtara ekleyin; karma uzunluğunu artırın |
| Bekleyen worker zaman aşımına uğradı | Çözen worker çöktü | solving durumundaki TTL otomatik olarak dolar (180 saniye) |
| Eski önbelleğe alınmış sonuçlar | Token'ın süresi doldu ama önbellek hâlâ geçerli | Sonuç önbelleği TTL'sini token ömründen daha kısa ayarlayın (reCAPTCHA için 60 saniye) |
| Set işleminde yarış durumu | İki worker aynı anda kontrol ediyor | Atomik kilit edinimi için SET NX (var değilse ayarla) kullanın |
Bu dört düzeltmeyi uyguladıktan sonra sıra, tasarrufu rakamlarla göstermeye gelir.
Tekilleştirme Tasarrufunu Ölçmek
Rakam olmadan bu katmanı yöneticinize veya kendinize kanıtlayamazsınız — sayaçları tutun:
def track_dedup_stats(source):
"""Increment counters for dedup tracking."""
today = time.strftime("%Y-%m-%d")
r.hincrby(f"dedup:stats:{today}", source, 1)
r.expire(f"dedup:stats:{today}", 7 * 86400)
def get_dedup_report():
today = time.strftime("%Y-%m-%d")
stats = r.hgetall(f"dedup:stats:{today}")
total = sum(int(v) for v in stats.values())
saved = int(stats.get("dedup_cache", 0)) + int(stats.get("dedup_wait", 0))
return {
"total_requests": total,
"deduplicated": saved,
"savings_pct": f"{saved / total * 100:.1f}%" if total else "0%",
"breakdown": stats
}
savings_pct metriğini paneliniz veya haftalık rapor akışınıza ekleyin. %15–20 aralığında sabitlenen bir tasarruf oranı, worker mimarinizde bir koordinasyon sorunu olduğunun güçlü bir işaretidir — anahtar TTL'sini veya worker sayısını gözden geçirmenin zamanı gelmiş demektir.
SSS
Tekilleştirme kaç eşzamanlı worker'dan sonra fark yaratır?
Aynı sitekey ve pageurl kombinasyonunu hedefleyen birden fazla worker çalıştırdığınız andan itibaren. %10'luk bir tekilleştirme oranı bile büyük ölçekte belirgin miktarda API kredisi tasarrufu sağlar ve boşa harcanan çözüm süresini ortadan kaldırır.
Dedup TTL süresi (180 saniye) nasıl belirlenir?
TTL, bir CAPTCHA'nın tipik çözüm süresinden biraz uzun tutulmalıdır — çoğu senaryoda 120–180 saniye yeterlidir. Çok kısa bir TTL çözüm bitmeden kilidi düşürüp yinelenen isteklere yol açar; çok uzun bir TTL ise çökmüş bir worker'ın kilidini gereksiz yere uzatır.
Tekilleştirme hangi CAPTCHA türlerinde işe yarar?
sitekey ve pageurl ile tanımlanan her türde çalışır: reCAPTCHA v2/v3, Cloudflare Turnstile ve GeeTest v3 dahil. Görsel CAPTCHA'larda ise anahtar, resim içeriğinin karma değerine dayanır.
Kuyruk mesajı yeniden oynatılırsa ne olur?
En az bir kez teslimat ilkesiyle (at-least-once delivery) çalışan kuyruklarda aynı mesaj birden fazla kez işlenebilir. Tekilleştirme anahtarı devredeyken, yeniden oynatılan mesaj solving veya solved durumundaki kaydı bulur ve yeni bir API çağrısı yapmadan aynı sonucu döndürür.
Görsel CAPTCHA'ları da tekilleştirmeli miyim?
Evet — resim içeriğinin karma değerini anahtarın parçası olarak kullanın. Aynı görsel her zaman aynı metni döndürdüğü için tekilleştirme burada da etkilidir.
Sırada Ne Var?
Yinelenen CAPTCHA çözümü için ödeme yapmayı durdurun: CaptchaAI API anahtarınızı alın ve tekilleştirme katmanını bugün devreye sokun.
İlgili kılavuzlar: