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.gather sobre 5.000 URLs, ou Promise.all sobre 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-After e adicione jitter.
  • Tratar uma franquia diária ou periódica como uma espera. plan_limit_browser_daily é redefinido à meia-noite UTC, plan_limit_credits e plan_limit_bandwidth no final do período de faturamento. Interrompa a execução e leia resets_at no 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

Atualizado em: 10 de setembro de 2026