Executar Requests em Paralelo
O Que Você Vai Aprender
Como executar um grande lote de requests FourA simultaneamente sem receber erros 429, limitando quantas chamadas você mantém abertas e reagindo a uma recusa em vez de repeti-la.
Pré-requisitos
- Uma chave de API da FourA (obtenha uma aqui)
- Python 3.9+ com
requests, ou Node.js 18+
Os Limites com os Quais Você Está Lidando
Seu plano possui dois limites máximos por endpoint: quantas requests podem ser executadas ao mesmo tempo e quantas podem iniciar por minuto (o Browser tem um limite diário em vez de um limite por minuto). Single, Proxy e Browser têm seus próprios números, e a aba Limits & Features em Usage & Limits lista esses valores.
A request que ultrapassa o teto de simultaneidade retorna como HTTP 429 com X-FourA-Limit: plan_limit_concurrency e o limite no corpo:
{
"error": "Concurrency limit reached: your plan allows 50 simultaneous single request(s). Retry when an in-flight request finishes.",
"reason": "plan_limit_concurrency",
"documentation": "https://foura.ai/prices",
"limit": 50,
"in_flight": 51,
"retry_after_seconds": 1
}
A requisição que ultrapassa o teto por minuto retorna com X-FourA-Limit: plan_limit_rate, e retry_after_seconds informa quanto tempo falta para que mais uma requisição seja aceita (de 1 a 120 segundos).
Ambas as recusas são imediatas. A FourA não coloca a chamada em fila para entregá-la mais tarde, portanto nada é gasto e nada é faturado. Uma chamada recusada não conta para o minuto deslizante, de modo que uma conta que solicita um pouco acima do seu teto ainda obtém o teto total. Uma tempestade de retentativas tem uma resposta própria: quando chamadas recusadas continuam chegando a mais que o dobro do seu teto por minuto, todas as chamadas nesse endpoint são recusadas com cooldown: true e retry_after_seconds: 30; portanto, pause durante esses 30 segundos. Referência completa dos campos: Rate Limits.
Dois pontos importantes que frequentemente passam despercebidos:
- Uma chamada
POST /api/auto/não ocupa nenhum slot por si só, mas cada subchamada Single, Proxy e Browser que ela faz para você ocupa. Uma chamada auto pode ocupar mais de um slot enquanto sua sequência é executada. (No seu dashboard, a chamada auto é a requisição; as subchamadas são as tentativas.) - Uma requisição Browser ocupa seu slot pelo tempo que a página leva para renderizar, o que é muito mais longo do que uma requisição Single. Um lote de chamadas browser atinge o teto com menos requisições do que um lote de chamadas single.
Passo 1: Limite Sua Própria Concorrência
Escolha um número abaixo do teto para o endpoint que você está chamando e mantenha-o. Um pool de workers faz isso em uma única linha:
import os
import concurrent.futures
import requests
API = "https://eu.api.foura.ai/api/single/"
KEY = os.environ["FOURA_API_KEY"]
HEADERS = {"X-API-Key": KEY, "Content-Type": "application/json"}
# Below your plan's Single concurrency, so a slow request never pushes the batch over it.
MAX_IN_FLIGHT = 30
def fetch_one(url):
resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)
return url, resp.status_code, resp.json()
def fetch_all(urls):
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_IN_FLIGHT) as pool:
for outcome in concurrent.futures.as_completed(pool.submit(fetch_one, u) for u in urls):
results.append(outcome.result())
return results
max_workers é todo o mecanismo. O pool nunca tem mais chamadas abertas do que isso, portanto o lote pode ter um milhão de URLs e ainda assim permanecer abaixo do limite.
Deixe uma margem de segurança. Se dois processos do seu lado usam a mesma conta, mesmo com chaves de API diferentes, eles compartilham o mesmo teto, então dê a cada um metade. Se um destino rápido permitir que o pool conclua requests mais rápido do que um minuto permite, controle o ritmo dos workers para ficar abaixo do teto por minuto também.
Passo 2: Use Backoff em Vez de Reenviar
Se uma recusa chegar, a resposta errada é reenviar o lote imediatamente. Cada chamada nele será recusada novamente, e as novas tentativas vão se acumular sobre as requests que já estavam em execução.
Aguarde primeiro. O tempo de espera está no header Retry-After e em retry_after_seconds no corpo:
import time
# Plan limits that no short wait will clear.
STOP_ON = {"plan_limit_browser_daily", "plan_limit_credits", "plan_limit_bandwidth", "plan_limit_feature", "plan_limit_premium"}
def fetch_one(url, attempts=4):
for attempt in range(attempts):
resp = requests.post(API, headers=HEADERS, json={"method": "GET", "url": url}, timeout=60)
limit = resp.headers.get("X-FourA-Limit")
if limit in STOP_ON:
raise RuntimeError(f"stopped by {limit}")
if resp.status_code not in (429, 503):
return url, resp.status_code, resp.json()
header = resp.headers.get("Retry-After")
if header and header.isdigit():
wait = int(header)
else:
body = resp.json()
wait = body.get("retry_after_seconds") or body.get("retryAfter") or 2 ** attempt
time.sleep(wait)
return url, 429, {"error": "still refused after retries"}
Adicione jitter quando você estiver executando muitos workers. Sem isso, todo worker recusado no mesmo segundo acorda no mesmo segundo e é recusado novamente em conjunto.
import random
time.sleep(wait + random.uniform(0, 0.5))
Etapa 3: A mesma coisa em Node
const API = 'https://eu.api.foura.ai/api/single/';
const HEADERS = {
'X-API-Key': process.env.FOURA_API_KEY,
'Content-Type': 'application/json',
};
const MAX_IN_FLIGHT = 30;
async function fetchOne(url) {
const resp = await fetch(API, {
method: 'POST',
headers: HEADERS,
body: JSON.stringify({ method: 'GET', url }),
});
return { url, status: resp.status, body: await resp.json() };
}
async function fetchAll(urls) {
const queue = [...urls];
const results = [];
async function worker() {
while (queue.length) {
results.push(await fetchOne(queue.pop()));
}
}
await Promise.all(
Array.from({ length: Math.min(MAX_IN_FLIGHT, urls.length) }, worker)
);
return results;
}
Um número fixo de workers consumindo de uma única fila mantém exatamente essa quantidade de chamadas abertas, qualquer que seja o tamanho do lote.
Passo 4: Monitore o que você realmente está usando
Abra Uso e limites no dashboard. Os contadores em tempo real ao lado dos seus limites mostram a concorrência, a taxa por minuto e as requisições de navegador por dia conforme mudam, permitindo dimensionar MAX_IN_FLIGHT com base em uma execução real em vez de uma estimativa.
O Log de atividades também registra recusas. Uma execução concluída com o número correto de linhas, mas com vários resultados rate_limit dispersos, estava limitando a si mesma via rate limit.
Erros comuns
- Disparar a lista inteira de uma vez.
asyncio.gatherem 5.000 URLs, ouPromise.allem um array ilimitado, abre 5.000 chamadas. Limite o pool, não a lista. - Tentar novamente em paralelo. Reenviar cada chamada recusada no exato momento da recusa reproduz o pico que causou a falha. Aguarde o
Retry-Aftere adicione jitter. - Tratar uma franquia diária ou do período como uma simples espera.
plan_limit_browser_dailyé zerado à meia-noite UTC,plan_limit_creditseplan_limit_bandwidthno fim do período de faturamento. Interrompa a execução e leiaresets_atno corpo da resposta quando presente. - Contar chamadas auto como apenas um slot cada. O escalonamento interno do
/api/auto/faz subchamadas reais, e são elas que contam para os limites máximos. - Compartilhar uma chave entre processos sem dividir o orçamento. Os limites máximos são por conta, não por processo.
- Dimensionar um único pool para todos os endpoints. Single, Proxy e Browser têm, cada um, seu próprio número de concorrência. Um pool dimensionado para Single excede o limite menor de Browser.
Conteúdo relacionado
- Rate Limits: Cada formato de recusa e o significado de cada campo
- Response Headers:
X-FourA-LimiteRetry-After - Resultados de requisição: Por que um resultado
rate_limitnunca é faturado - Uso e limites: Onde encontrar os contadores em tempo real e os números do seu plano
- Smart Fetch (Auto): Como uma única chamada auto se divide em várias subchamadas