Executar Requests em Paralelo
O que você aprenderá
Como executar um grande lote de requests da 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ê trabalha
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 possui uma cota por dia em vez de por minuto). Single, Proxy e Browser têm seus próprios números, e a aba Limits & Features em Usage & Limits lista todos eles.
A request que excede o limite de simultaneidade retorna como HTTP 429 com X-FourA-Limit: plan_limit_concurrency e o teto no corpo da resposta:
{
"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
}
Aquela que ultrapassar o teto por minuto retorna com X-FourA-Limit: plan_limit_rate e retry_after_seconds contando até o final do minuto.
Ambas as recusas são imediatas. O FourA não enfileira a chamada para entregá-la depois, portanto nada é gasto e nada é cobrado. No entanto, chamadas recusadas contam para o minuto móvel, de modo que uma tempestade de retentativas estende seu próprio cooldown. Referência completa dos campos: Rate Limits.
Dois fatores importantes que muitos não percebem:
- Uma chamada
POST /api/auto/não é contabilizada isoladamente, mas cada subchamada Single, Proxy e Browser que ela faz por você é. Uma chamada auto pode ocupar mais de um slot enquanto sua sequência é executada. - Uma request Browser ocupa seu slot durante todo o tempo de renderização da página, o que é muito mais longo do que uma request Single. Um lote de chamadas browser atinge o teto com menos requests do que um lote de chamadas single.
Step 1: Cap Your Own Concurrency
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 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. Se dois processos do seu lado compartilham uma chave de API, eles compartilham o mesmo teto, então dê a cada um a metade. Se um destino rápido permitir que o pool conclua as requisições mais rápido do que um minuto permite, controle o ritmo dos workers para permanecer abaixo do teto por minuto também.
Passo 2: Faça Backoff em Vez de Reenviar
Se uma recusa acontecer, a resposta errada é reenviar o lote imediatamente. Cada chamada nele será recusada novamente, e as novas tentativas vão se acumular sobre as requisições que já estavam em execução.
Aguarde primeiro. O tempo de espera está no header Retry-After e em retry_after_seconds no body:
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 estiver executando muitos workers. Sem isso, cada 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 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, para que você possa dimensionar MAX_IN_FLIGHT com base em uma execução real em vez de uma suposição.
O Registro de atividades também registra recusas. Uma execução que terminou com o número correto de linhas, mas com uma dispersão de resultados rate_limit, estava sofrendo limitação por conta própria.
Erros comuns
- Disparar a lista inteira de uma vez.
asyncio.gathersobre 5.000 URLs, ouPromise.allsobre um array sem limites, abre 5.000 chamadas. Limite o pool, não a lista. - Tentar novamente em paralelo. Reenviar cada chamada recusada no momento em que é recusada reproduz o pico que causou a recusa. Aguarde o
Retry-Aftere adicione jitter. - Tratar uma franquia diária ou periódica como uma espera.
plan_limit_browser_dailyé redefinido à meia-noite UTC,plan_limit_creditseplan_limit_bandwidthno final do período de faturamento. Interrompa a execução e leiaresets_atno corpo quando houver um. - Contar chamadas auto como um único slot cada. A lógica de fallback interna do
/api/auto/faz subchamadas reais, e são elas que os limites contam. - Compartilhar uma chave entre processos sem dividir o orçamento. Os limites são por conta, não por processo.
- Dimensionar um único pool para todos os endpoints. Single, Proxy e Browser têm seus próprios números de concorrência. Um pool dimensionado para Single ultrapassa o limite menor de Browser.
Relacionado
- Limites de taxa: Todos os formatos de recusa e o que cada campo significa
- Cabeçalhos de resposta:
X-FourA-LimiteRetry-After - Resultados de requisições: Por que um resultado
rate_limitnunca é cobrado - Uso e limites: Onde ficam os contadores em tempo real e os números do seu plano
- Smart Fetch (Auto): Como uma chamada auto se transforma em várias subchamadas