Limites de peso em APIs de exchanges cripto: como coletar dados de mercado com proxies (exemplo Binance)

Guia prático para coletar dados públicos de mercado da Binance REST API em escala usando proxies residenciais rotativos, com exemplos em Python e Node.js, controle de peso e mitigação de banimentos por IP.

How to Scrape the Binance REST API with Proxies: A Developer Guide
Neste artigo

Se você está construindo pipelines de dados quant ou alimentando modelos de trading com candles e order books da Binance, já esbarrou no limite de peso por IP. Este guia mostra como raspar a API REST da Binance com proxies de forma eficiente, cobrindo endpoints públicos, rate limiting baseado em peso, rotação de IPs e padrões de produção com retries e backoff exponencial.

Aviso de conformidade: Este artigo cobre apenas dados públicos de mercado via endpoints REST abertos. Respeite os limites de peso documentados, leia os Termos de Uso da Binance e prefira acesso oficial via API quando os termos exigirem. Não incentivamos coleta de dados privados, evasão de restrições regulatórias nem violação de ToS.

Por que raspar a API REST da Binance com proxies é necessário

A Binance opera uma das maiores APIs públicas de cripto do mundo. Os endpoints REST em api.binance.com são gratuitos, não exigem autenticação para dados de mercado e retornam JSON estruturado. O problema é que a Binance usa um sistema de limite de taxa baseado em peso por IP, não por conta. Cada endpoint consome uma quantidade diferente de "peso" e o orçamento por IP é de aproximadamente 6.000 pesos por minuto.

Quando você consome esse orçamento, recebe um HTTP 429 Too Many Requests. Se persistir, a Binance escala para um HTTP 418 (IP banido) com um cabeçalho Retry-After indicando quanto tempo esperar — tipicamente entre 2 minutos e 3 dias, conforme a severidade. É por isso que desenvolvedores precisam de proxies: distribuir o peso across múltiplos IPs é a única forma de escalar a coleta sem ser banido.

Segundo a documentação oficial da Binance Spot API, cada resposta inclui o cabeçalho X-MBX-USED-WEIGHT-1M, que indica quantos pesos você consumiu no último minuto. Monitorar esse valor é essencial para throttling dinâmico.

Endpoints-chave de dados públicos de mercado

EndpointDescriçãoPeso (aprox.)Uso típico
GET /api/v3/klinesCandlesticks OHLCV1–2Backfill histórico, gráficos
GET /api/v3/depthOrder book snapshot5–20 (depende do limit)Microestrutura de mercado
GET /api/v3/ticker/24hrEstatísticas 24h1–40 (por symbol ou all)Dashboards, screening
GET /api/v3/ticker/pricePreço atual1–2Monitoramento em tempo real

O endpoint /api/v3/depth é o mais custoso: com limit=1000, consome 20 pesos por requisição. Em polling contínuo a cada 500ms, você queima 2.400 pesos por minuto — quase metade do orçamento em um único IP. Com limit=5000, são 50 pesos por chamada, esgotando o orçamento em ~2,4 minutos.

Arquitetura de coleta com proxies residenciais rotativos

A estratégia é simples: cada IP recebe seu próprio orçamento de 6.000 pesos/min. Com N IPs residenciais rotativos, seu orçamento efetivo é N × 6.000. Com 50 IPs, são 300.000 pesos por minuto — suficiente para polling agressivo de depth em múltiplos pares.

Proxies residenciais são preferidos sobre datacenter porque a Binance frequentemente penaliza ranges de IPs de datacenter conhecidos. IPs residenciais aparecem como tráfego de usuários finais legítimos, reduzindo a probabilidade de 429 e 418.

Bypass do geo-split Binance.com vs Binance.US

Usuários nos EUA frequentemente recebem HTTP 451 Unavailable For Legal Reasons ao acessar api.binance.com, devido ao bloqueio geográfico. Com geo-targeting no username do ProxyHat (-country-US), você pode rotear através de IPs de outros países para acessar endpoints globais, ou usar IPs dos EUA para acessar api.binance.us quando apropriado.

Configuração do ProxyHat: HTTP e SOCKS5

O ProxyHat oferece gateway único em gate.proxyhat.com com porta 8080 para HTTP e 1080 para SOCKS5. A rotação e geo-targeting são controlados via flags no username.

Formatos de URL

# HTTP — rotação automática por requisição
http://USERNAME:PASSWORD@gate.proxyhat.com:8080

# HTTP — geo-targeting EUA
http://USERNAME-country-US:PASSWORD@gate.proxyhat.com:8080

# HTTP — sessão fixa (sticky) para paginação
http://USERNAME-sid-abc123:PASSWORD@gate.proxyhat.com:8080

# SOCKS5 — para clientes que exigem SOCKS
socks5://USERNAME:PASSWORD@gate.proxyhat.com:1080

Exemplo 1: curl com proxy bruto e monitoramento de peso

# Consultar klines com proxy rotativo e capturar peso usado
curl -x http://USERNAME:PASSWORD@gate.proxyhat.com:8080 \
  -s -D - \
  'https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1m&limit=100' \
  -o /dev/null | grep -i 'x-mbx-used-weight'

O cabeçalho X-MBX-USED-WEIGHT-1M na resposta diz exatamente quantos pesos aquele IP consumiu. Em rotação por requisição, cada IP começa com peso próximo de zero.

Exemplo 2: Python requests com rotação por requisição via ProxyHat SDK

import requests
import time
import random
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

PROXYHAT_USER = 'seu_usuario'
PROXYHAT_PASS = 'sua_senha'

# Cada request_id único força novo IP

def get_proxy_url(request_id: str, country: str = None) -> str:
    username = f'{PROXYHAT_USER}-sid-{request_id}'
    if country:
        username += f'-country-{country}'
    return f'http://{username}:{PROXYHAT_PASS}@gate.proxyhat.com:8080'


def fetch_klines(symbol: str, interval: str, limit: int, max_retries: int = 5) -> list:
    url = f'https://api.binance.com/api/v3/klines'
    params = {'symbol': symbol, 'interval': interval, 'limit': limit}

    for attempt in range(max_retries):
        request_id = f'klines-{int(time.time())}-{random.randint(1000,9999)}'
        proxies = {
            'http': get_proxy_url(request_id, country='DE'),
            'https': get_proxy_url(request_id, country='DE'),
        }
        try:
            resp = requests.get(url, params=params, proxies=proxies, timeout=10)
            used_weight = resp.headers.get('X-MBX-USED-WEIGHT-1M', '?')
            logger.info(f'Peso usado no IP: {used_weight} | attempt={attempt}')

            if resp.status_code == 200:
                return resp.json()
            elif resp.status_code == 429:
                retry_after = int(resp.headers.get('Retry-After', 5))
                logger.warning(f'429 recebido. Esperando {retry_after}s')
                time.sleep(retry_after)
            elif resp.status_code == 418:
                retry_after = int(resp.headers.get('Retry-After', 60))
                logger.error(f'418 banido neste IP. Esperando {retry_after}s')
                time.sleep(retry_after)
            else:
                resp.raise_for_status()
        except requests.RequestException as e:
            backoff = (2 ** attempt) + random.uniform(0, 1)
            logger.warning(f'Erro: {e}. Backoff {backoff:.1f}s')
            time.sleep(backoff)

    raise RuntimeError(f'Falha após {max_retries} tentativas')


# Uso
data = fetch_klines('BTCUSDT', '1m', 500)
print(f'Candles recebidos: {len(data)}')

Neste exemplo, cada chamada gera um novo sid-ID, forçando o ProxyHat a atribuir um novo IP residencial. O monitoramento do cabeçalho X-MBX-USED-WEIGHT-1M permite throttling adaptativo.

Exemplo 3: Python httpx async com sessão fixa para backfill paginado de klines

import httpx
import asyncio
import logging
from datetime import datetime, timedelta

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

PROXYHAT_USER = 'seu_usuario'
PROXYHAT_PASS = 'sua_senha'

# Sessão fixa mantém o MESMO IP durante todo o backfill
# Isso é crítico: se o IP mudar no meio da paginação,
# o peso acumulado é perdido e você recomeça do zero.

SESSION_ID = 'backfill-btcusdt-001'
BASE_PROXY = (
    f'http://{PROXYHAT_USER}-sid-{SESSION_ID}:'
    f'{PROXYHAT_PASS}@gate.proxyhat.com:8080'
)


class WeightAwareThrottler:
    def __init__(self, max_weight: int = 5000):
        self.max_weight = max_weight
        self.current_weight = 0

    def check(self, used_weight_header: str):
        if used_weight_header:
            self.current_weight = int(used_weight_header)
        if self.current_weight >= self.max_weight:
            sleep_time = 62  # reseta a cada ~60s
            logger.info(f'Peso {self.current_weight} >= limite. Dormindo {sleep_time}s')
            asyncio.get_event_loop().run_until_complete(
                asyncio.sleep(sleep_time)
            )


async def fetch_klines_page(client, symbol, interval, start_time, end_time):
    params = {
        'symbol': symbol,
        'interval': interval,
        'startTime': start_time,
        'endTime': end_time,
        'limit': 1000,
    }
    resp = await client.get(
        'https://api.binance.com/api/v3/klines',
        params=params
    )
    used = resp.headers.get('x-mbx-used-weight-1m', '0')
    logger.info(f'Peso usado: {used}')
    if resp.status_code == 429:
        retry_after = int(resp.headers.get('retry-after', 5))
        await asyncio.sleep(retry_after)
        return await fetch_klines_page(client, symbol, interval, start_time, end_time)
    resp.raise_for_status()
    return resp.json()


async def backfill_klines(symbol: str, interval: str, days: int = 7):
    throttler = WeightAwareThrottler(max_weight=5000)
    start = int((datetime.utcnow() - timedelta(days=days)).timestamp() * 1000)
    end = int(datetime.utcnow().timestamp() * 1000)

    all_candles = []
    async with httpx.AsyncClient(
        proxy=BASE_PROXY,
        timeout=15.0,
        limits=httpx.Limits(max_connections=5, max_keepalive_connections=2),
    ) as client:
        current = start
        while current < end:
            page_end = min(current + (1000 * 60_000), end)  # 1000 candles de 1m
            candles = await fetch_klines_page(client, symbol, interval, current, page_end)
            if not candles:
                break
            all_candles.extend(candles)
            current = candles[-1][0] + 1  # next open time + 1ms
            throttler.check(client.headers.get('x-mbx-used-weight-1m', '0'))
            await asyncio.sleep(0.25)  # 250ms entre chamadas

    logger.info(f'Total candles coletados: {len(all_candles)}')
    return all_candles


# Executar
asyncio.run(backfill_klines('BTCUSDT', '1m', days=3))

Aqui usamos uma sessão fixa (-sid-backfill-btcusdt-001) para manter o mesmo IP durante toda a paginação. Isso preserva o contexto de peso acumulado e evita resets desnecessários. O WeightAwareThrottler monitora o peso e pausa antes de atingir o limite de 6.000.

Exemplo 4: Node.js com axios e rotação de IPs

const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');

const PROXYHAT_USER = 'seu_usuario';
const PROXYHAT_PASS = 'sua_senha';

function buildProxy(requestId, country = null) {
  let username = `${PROXYHAT_USER}-sid-${requestId}`;
  if (country) username += `-country-${country}`;
  return `http://${username}:${PROXYHAT_PASS}@gate.proxyhat.com:8080`;
}

async function fetchDepth(symbol, limit = 100, maxRetries = 5) {
  const url = 'https://api.binance.com/api/v3/depth';

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const requestId = `depth-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
    const proxyUrl = buildProxy(requestId, 'JP');
    const agent = new HttpsProxyAgent(proxyUrl);

    try {
      const resp = await axios.get(url, {
        params: { symbol, limit },
        httpsAgent: agent,
        timeout: 10000,
        headers: { 'User-Agent': 'Mozilla/5.0' },
      });

      const usedWeight = resp.headers['x-mbx-used-weight-1m'];
      console.log(`Peso: ${usedWeight} | IP rotacionado: ${requestId}`);

      return resp.data;
    } catch (err) {
      if (err.response) {
        const status = err.response.status;
        const retryAfter = err.response.headers['retry-after'] || 5;

        if (status === 429) {
          console.warn(`429: esperando ${retryAfter}s`);
          await new Promise(r => setTimeout(r, retryAfter * 1000));
        } else if (status === 418) {
          console.error(`418 banido neste IP. Esperando ${retryAfter}s`);
          await new Promise(r => setTimeout(r, retryAfter * 1000));
        } else {
          throw err;
        }
      } else {
        const backoff = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
        console.warn(`Erro de rede: ${err.message}. Backoff ${backoff}ms`);
        await new Promise(r => setTimeout(r, backoff));
      }
    }
  }
  throw new Error(`Falha após ${maxRetries} tentativas`);
}

// Polling com concorrência limitada
async function pollDepth(symbols, intervalMs = 1000) {
  const MAX_CONCURRENT = 5;
  const queue = [...symbols];
  const results = [];

  async function worker() {
    while (queue.length > 0) {
      const sym = queue.shift();
      try {
        const data = await fetchDepth(sym, 100);
        results.push({ symbol: sym, data });
      } catch (e) {
        console.error(`Falha em ${sym}: ${e.message}`);
      }
      await new Promise(r => setTimeout(r, intervalMs));
    }
  }

  await Promise.all(Array.from({ length: MAX_CONCURRENT }, () => worker()));
  return results;
}

// Uso
pollDepth(['BTCUSDT', 'ETHUSDT', 'SOLUSDT'], 800).then(() => {
  console.log('Coleta concluída');
});

Exemplo 5: Throttling com peso adaptativo e circuit breaker

import httpx
import asyncio
import logging
import random

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

PROXYHAT_USER = 'seu_usuario'
PROXYHAT_PASS = 'sua_senha'

class BinanceScraper:
    def __init__(self, max_concurrent=10, weight_threshold=5500):
        self.max_concurrent = max_concurrent
        self.weight_threshold = weight_threshold
        self.circuit_open = False
        self.consecutive_failures = 0
        self.semaphore = asyncio.Semaphore(max_concurrent)

    def _proxy_url(self, session_id, country=None):
        user = f'{PROXYHAT_USER}-sid-{session_id}'
        if country:
            user += f'-country-{country}'
        return f'http://{user}:{PROXYHAT_PASS}@gate.proxyhat.com:8080'

    async def _request(self, endpoint, params, session_id, country=None):
        async with self.semaphore:
            if self.circuit_open:
                logger.warning('Circuit breaker aberto. Aguardando 30s')
                await asyncio.sleep(30)
                self.circuit_open = False

            proxy = self._proxy_url(session_id, country)
            async with httpx.AsyncClient(proxy=proxy, timeout=10) as client:
                resp = await client.get(
                    f'https://api.binance.com{endpoint}',
                    params=params
                )

                used_weight = int(resp.headers.get('x-mbx-used-weight-1m', 0))

                # Throttling adaptativo baseado no peso
                if used_weight > self.weight_threshold:
                    sleep_time = max(60 - (used_weight // 100), 5)
                    logger.info(f'Peso {used_weight} alto. Dormindo {sleep_time}s')
                    await asyncio.sleep(sleep_time)

                if resp.status_code == 200:
                    self.consecutive_failures = 0
                    return resp.json()
                elif resp.status_code == 429:
                    retry_after = int(resp.headers.get('retry-after', 10))
                    await asyncio.sleep(retry_after)
                    return await self._request(endpoint, params, session_id, country)
                elif resp.status_code == 418:
                    self.consecutive_failures += 1
                    if self.consecutive_failures >= 3:
                        self.circuit_open = True
                    retry_after = int(resp.headers.get('retry-after', 120))
                    logger.error(f'IP banido. Esperando {retry_after}s')
                    await asyncio.sleep(retry_after)
                    # Novo IP via nova session
                    new_sid = f'{session_id}-retry-{random.randint(100,999)}'
                    return await self._request(endpoint, params, new_sid, country)
                else:
                    resp.raise_for_status()

    async def scrape_ticker_24hr(self, symbols):
        tasks = []
        for sym in symbols:
            sid = f'ticker24-{sym}-{random.randint(0,99999)}'
            tasks.append(
                self._request(
                    '/api/v3/ticker/24hr',
                    {'symbol': sym},
                    sid,
                    country='SG'
                )
            )
        return await asyncio.gather(*tasks, return_exceptions=True)


# Uso
async def main():
    scraper = BinanceScraper(max_concurrent=10, weight_threshold=5500)
    symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'SOLUSDT', 'XRPUSDT']
    results = await scraper.scrape_ticker_24hr(symbols)
    for sym, result in zip(symbols, results):
        if isinstance(result, Exception):
            logger.error(f'{sym}: {result}')
        else:
            logger.info(f'{sym}: var {result.get("priceChangePercent", "N/A")}%')

asyncio.run(main())

Quando usar WebSocket streams em vez de REST polling

Para dados em tempo real, a Binance oferece WebSocket streams em wss://stream.binance.com:9443. Para klines e depth contínuos, WebSocket é superior porque:

  • Não consome peso REST: streams são rate-limited separadamente (máx 5 conexões por IP, 200 streams por conexão).
  • Latência menor: pushes em vez de polling. Atualizações de depth chegam em ~10ms vs 500ms+ de polling.
  • Menos overhead: uma conexão WebSocket substitui dezenas de polls REST por minuto.

Use REST para backfill histórico (klines paginadas), snapshots pontuais e endpoints sem equivalente WebSocket. Use WebSocket para monitoramento contínuo de preço, depth incremental e trade streams.

Erros comuns e armadilhas

  • Ignorar o cabeçalho X-MBX-USED-WEIGHT-1M: sem monitorar o peso, você descobre o limite só quando recebe 429.
  • Usar datacenter proxies para depth polling: ranges de datacenter são frequentemente penalizados. Prefira residenciais.
  • Não respeitar Retry-After: receber 429 e continuar batendo garante um 418 com ban de horas ou dias.
  • Mudar de IP no meio de paginação: se você está paginando klines com startTime/endTime, trocar de IP reinicia o contador de peso mas pode causar gaps se a paginação depende de estado do servidor.
  • Concorrência sem limite: abrir 50 conexões simultâneas sem semáforo esgota o orçamento em segundos. Use asyncio.Semaphore ou equivalente.
  • Não tratar HTTP 451: se você acessa api.binance.com de um IP dos EUA sem geo-targeting, pode receber 451. Configure -country-XX apropriadamente.

Configuração específica do ProxyHat

Para começar com o ProxyHat, acesse o dashboard de preços e escolha um plano residencial. A configuração é imediata — sem SDK proprietário obrigatório, apenas HTTP/SOCKS5 com flags no username.

Consulte a documentação oficial do ProxyHat para detalhes sobre geo-targeting por cidade, rotação por header e integrações avançadas.

Para casos de uso de web scraping em geral, veja nosso guia de web scraping com proxies. Para rastreamento de SERPs, confira SERP tracking com proxies. A lista completa de localizações disponíveis está em proxy locations.

Pontos-chave

  • Peso, não requisições: a Binance limita por peso (6.000/min por IP), não por count. /api/v3/depth com limit=1000 custa 20 pesos por chamada.
  • Monitore X-MBX-USED-WEIGHT-1M: esse cabeçalho é seu medidor de combustível. Sem ele, você opera no escuro.
  • 429 é aviso, 418 é banimento: respeite Retry-After sempre. Ignorar 429 leva a 418 com bans de horas a dias.
  • Residenciais > datacenter: IPs residenciais rotativos distribuem peso across múltiplos orçamentos e evitam penalizações de ranges conhecidos.
  • Sessão fixa para paginação: use -sid-XXX para manter o mesmo IP durante backfill. Rotação por requisição para coletas independentes.
  • WebSocket para tempo real: para monitoramento contínuo, streams são mais eficientes que REST polling.
  • Geo-targeting resolve 451: use -country-XX para bypassar bloqueios regionais e acessar endpoints globais.

Perguntas frequentes

Por que raspar a API REST da Binance com proxies importa para usuários de proxy?

Porque a Binance penaliza agressivamente IPs que excedem o orçamento de peso. Sem proxies, um único IP é banido em minutos ao fazer polling de depth. Com proxies residenciais rotativos, cada IP tem seu próprio orçamento, multiplicando a capacidade de coleta. Além disso, o geo-targeting via username (-country-XX) resolve bloqueios regionais (HTTP 451) que afetam usuários em jurisdições restritas como os EUA.

Qual tipo de proxy funciona melhor para raspar a API REST da Binance?

Proxies residenciais rotativos são os melhores porque aparecem como tráfego de usuários finais legítimos e não são penalizados como ranges de datacenter. Para backfill paginado, use sessões fixas (-sid-XXX) para manter o mesmo IP e preservar o contexto de peso. Para coletas independentes, rotação por requisição é ideal. SOCKS5 na porta 1080 é útil para clientes que exigem esse protocolo.

Como evitar bloqueios ao raspar a Binance REST API?

Monitore o cabeçalho X-MBX-USED-WEIGHT-1M em cada resposta e implemente throttling adaptativo: se o peso se aproximar de 5.500, pause por 60s. Trate 429 respeitando Retry-After e nunca continue após receber 418. Use backoff exponencial com jitter para erros de rede. Limite a concorrência com semáforos (10–20 conexões simultâneas). Rotacione IPs por requisição para endpoints independentes e use sessões fixas para paginação.

Quando devo usar WebSocket em vez de REST polling na Binance?

Use WebSocket (wss://stream.binance.com:9443) para monitoramento contínuo de preço, depth incremental e trade streams. WebSocket não consome peso REST, tem latência menor (~10ms vs 500ms+) e substitui dezenas de polls por minuto. Use REST para backfill histórico de klines paginadas, snapshots pontuais e endpoints sem equivalente WebSocket como /api/v3/ticker/24hr para todos os símbolos de uma vez.

Pronto para começar?

Proxies residenciais, ISP e móveis em mais de 195 países. Crie uma conta grátis.

Criar conta grátis
← Voltar ao Blog