Concorrência, não créditos, decide quanto tempo seu job de scraping leva

O mesmo job de 200.000 requisições leva 22h com 5 slots, 4,4h com 25. Por que os planos limitam a concorrência, e um worker pool correto em Python.

Duas contas podem ter exatamente o mesmo saldo de créditos e terminar o mesmo job em velocidades completamente diferentes. O número que decide quanto tempo um job em lote leva é a concorrência, quantas requisições você pode ter em andamento ao mesmo tempo, e geralmente não é o número que as pessoas checam primeiro.

A conta

Digamos que um job tenha 200.000 requisições, e cada uma leve cerca de 2 segundos de ida e volta, uma média razoável para uma página renderizada ou uma busca com retries. O trabalho total é de 400.000 segundos. Quanto tempo isso leva para rodar depende inteiramente de quantos desses segundos você consegue gastar em paralelo:

Slots simultâneos Tempo total
1 111,1 horas (4,6 dias)
5 22,2 horas
10 11,1 horas
25 4,4 horas

Mesmo job, mesmo custo em créditos, uma diferença de 25x no tempo real entre 1 slot e 25. Se um job noturno precisa terminar antes do início do dia útil, a concorrência é o número a checar antes de fechar um cronograma, não o saldo de créditos.

Por que os planos limitam a concorrência, não só os créditos

Os créditos medem quanto trabalho você pode fazer no total. A concorrência mede quanto desse trabalho pode acontecer no mesmo instante, e existe por um motivo separado: cada requisição em andamento ocupa uma sessão de navegador, uma conexão de proxy ou ambos, em infraestrutura que custa dinheiro para ficar provisionada, esteja você perto do seu limite de créditos ou não.

Na FastAF, a concorrência escala com o pacote de créditos que você compra: Free roda 2 requisições simultâneas, cada pacote pago 5 (Starter, Growth, Scale e Pro), Business 25 (outubro de 2026). Uma conta Free consegue rodar duas requisições ao mesmo tempo; uma conta Business pode ter 25 abertas simultaneamente. Enviar mais requisições do que a concorrência do seu pacote não entra educadamente numa fila para ir mais rápido, começa a devolver respostas 429, então vale a pena desenhar o job em torno desse limite em vez de descobrir isso na tentativa e erro.

Escrevendo um worker pool que respeita o limite

O erro que realmente custa tempo é definir max_workers de forma otimista e deixar a lógica de retry disfarçar os 429s resultantes. Defina-o com a concorrência real do seu pacote e os 429s praticamente deixam de acontecer desde o início.

ThreadPoolExecutor, a opção mais simples para a maioria dos scripts:

import time
import requests
from concurrent.futures import ThreadPoolExecutor

API_KEY = "faf_live_..."  # sua chave FastAF
CONCURRENCY = 5  # de acordo com seu pacote: Free 2, de Starter a Pro 5, Business 25

def fetch(url, max_retries=5):
    for attempt in range(max_retries):
        resp = requests.get(
            "https://api.fastaf.co/v1/scrape",
            params={"url": url},
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=90,
        )
        if resp.status_code == 200:
            return resp.json()
        if resp.status_code == 429:
            wait = int(resp.headers.get("Retry-After", 2 ** attempt))
            time.sleep(wait)
            continue
        if resp.status_code in (502, 503, 504):
            time.sleep(2 ** attempt)  # falha temporária no upstream, não cobrada
            continue
        resp.raise_for_status()
    raise RuntimeError(f"desisti de {url} após {max_retries} tentativas")

urls = [...]  # seus 200.000 URLs
results = []
with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
    for result in pool.map(fetch, urls):
        results.append(result)

asyncio, se o resto do pipeline já for assíncrono, com um semáforo fazendo o mesmo trabalho que max_workers fazia acima:

import asyncio
import httpx

CONCURRENCY = 5
sem = asyncio.Semaphore(CONCURRENCY)

async def fetch(client, url, max_retries=5):
    async with sem:
        for attempt in range(max_retries):
            resp = await client.get(
                "https://api.fastaf.co/v1/scrape",
                params={"url": url},
                headers={"Authorization": f"Bearer {API_KEY}"},
                timeout=90,
            )
            if resp.status_code == 200:
                return resp.json()
            if resp.status_code == 429:
                wait = int(resp.headers.get("Retry-After", 2 ** attempt))
                await asyncio.sleep(wait)
                continue
            if resp.status_code in (502, 503, 504):
                await asyncio.sleep(2 ** attempt)
                continue
            resp.raise_for_status()
        raise RuntimeError(f"desisti de {url} após {max_retries} tentativas")

async def run(urls):
    async with httpx.AsyncClient() as client:
        return await asyncio.gather(*(fetch(client, u) for u in urls))

results = asyncio.run(run(urls))

O semáforo mantém exatamente o mesmo teto de concorrência que o max_workers acima; escolha o estilo que combina com o resto do seu código, não por motivos de correção.

As duas coisas que realmente causam 429s na prática

Workers acima do limite do pacote. O caso óbvio: max_workers=50 em um pacote Starter (5 simultâneas) significa que 45 de cada 50 requisições falham imediatamente com 429, e a lógica de backoff só torna isso lento em vez de rápido.

Um segundo processo que você esqueceu. Um cron job e um teste manual rodando contra a mesma chave de API ao mesmo tempo dobram sua concorrência efetiva sem que nenhum dos dois processos saiba do outro. Se você está vendo 429s mesmo com max_workers configurado corretamente, procure um segundo script rodando contra a mesma chave antes de assumir que o limite mudou.

Resumo

Antes de estimar quanto tempo um job em lote vai levar, cheque a concorrência do seu pacote, não só o saldo de créditos: 2 para Free, 5 para Starter, Growth, Scale e Pro, 25 para Business a partir de outubro de 2026. Defina o tamanho do seu worker pool com esse número, respeite o Retry-After em 429 e recue com atraso exponencial em 5xx. O tempo de execução de um job é (total de requisições x latência média) / slots simultâneos, e a concorrência é o único desses três números que você realmente consegue mudar comprando um pacote maior.