저는 작년에 고주파 트레이딩 봇을 개발하면서 Binance API의 1200 weight/분 제한에 정면으로 부딪혔습니다. 4시간 봉 캔들 500개를 한 번에 받아오려는 순간, 아래와 같은 응답이 쏟아지더군요.

HTTP 429 Too Many Requests
{"code": -1003, "msg": "Too much request weight used; current limit is 1200 request weight per minute. Please use the websocket for live updates to avoid polling the API."}

X-MBX-USED-WEIGHT-1M: 1200
X-MBX-ORDER-COUNT-10S: 50

이 글에서는 제가 실제로 적용해 봤던 가중치 추적 + 다중 API Key 라운드 로빈 폴링方案을 단계별로 공개합니다. 단순한 sleep 루프가 아니라, 응답 헤더에 포함된 X-MBX-USED-WEIGHT-1M 값을 실시간으로 모니터링하면서 키를 자동 스위칭하는 구조입니다.

1. Binance API 가중치(Weight) 시스템 이해

Binance는 단순한 요청 횟수 제한이 아니라, 엔드포인트별 가중치를 사용합니다. 한도(1200 weight/분)는 같지만, 엔드포인트마다 소모되는 weight가 다릅니다.

엔드포인트 소모 Weight 분당 최대 호출(단일 키)
GET /api/v3/depth (limit 5~100) 1~5 240~1200회
GET /api/v3/depth (limit 500~1000) 10~20 60~120회
GET /api/v3/trades 5 240회
GET /api/v3/klines (limit 1000) 2 600회
GET /api/v3/exchangeInfo 20 60회
POST /api/v3/order 1 1200회 (단, 주문 별도 제한 50/10초)

핵심은 응답 헤더의 X-MBX-USED-WEIGHT-1M입니다. 이 값이 1080(90%)을 넘어가면 곧 429가 오기 때문에, 사전에 다른 API Key로 전환해야 합니다.

2. 단일 키 + Sleep vs 다중 키 폴링方案 비교

전략 처리량(분당) 구현 난이도 장애 내성 권장 시나리오
단일 키 + 고정 sleep ~600 req 매우 쉬움 낮음 백테스트 1회성 수집
단일 키 + 가중치 동적 sleep ~1100 req 중간 중간 일반 트레이딩 봇
다중 키 라운드 로빈 ~3600 req (3키) 중간 높음 고주파 봇, 다계정 헷징
WebSocket 단독 무제한 (실시간) 높음 매우 높음 실시간 시세 전용
VIP 9 + 단일 키 ~180,000 req (분당 2400 weight × 75배) 쉬움 (단, 자격 요건) 높음 기관 트레이더

3. 다중 API Key 폴러 구현 (Python)

저는 운영 환경에서 3개의 서브계정 API Key를 발급받아 라운드 로빈 + 가중치 임계치 스위칭 방식으로 사용합니다. 핵심 로직은 다음과 같습니다.

# multi_key_binance.py
import time
import hmac
import hashlib
import requests
from urllib.parse import urlencode
from typing import List, Dict, Optional

class BinanceMultiKeyPoller:
    """
    다중 API Key 라운드 로빈 + 가중치 임계치 자동 스위칭
    Binance Spot API 기준
    """

    BASE_URL = "https://api.binance.com"
    WEIGHT_LIMIT = 1200           # 분당 최대 weight
    SWITCH_THRESHOLD = 0.85       # 85% 도달 시 키 전환
    SAFETY_MARGIN = 60            # 헤더 지연 감안한 여유분

    def __init__(self, api_keys: List[Dict[str, str]]):
        """
        api_keys 예시:
        [
            {"api_key": "KEY1", "api_secret": "SECRET1"},
            {"api_key": "KEY2", "api_secret": "SECRET2"},
            {"api_key": "KEY3", "api_secret": "SECRET3"},
        ]
        """
        self.keys = api_keys
        self.idx = 0
        self.weight = [0] * len(api_keys)
        self.last_reset = [time.time()] * len(api_keys)

    def _sign(self, params: Dict, secret: str) -> str:
        query = urlencode(params)
        return hmac.new(
            secret.encode(), query.encode(), hashlib.sha256
        ).hexdigest()

    def _pick_key(self) -> int:
        """가중치 여유 있는 키 선택"""
        now = time.time()
        # 60초 경과 시 weight 리셋
        for i in range(len(self.keys)):
            if now - self.last_reset[i] >= 60:
                self.weight[i] = 0
                self.last_reset[i] = now

        # 현재 키의 weight가 임계치 초과면 다음 키로
        attempts = 0
        while attempts < len(self.keys):
            cur = self.idx
            used = self.weight[cur]
            if used < self.WEIGHT_LIMIT * self.SWITCH_THRESHOLD:
                return cur
            self.idx = (self.idx + 1) % len(self.keys)
            attempts += 1
        return self.idx  # 전부 다 차면 그래도 현재 키

    def call(self, method: str, endpoint: str,
             params: Optional[Dict] = None,
             signed: bool = False) -> Dict:
        params = params or {}
        headers = {}
        key_idx = self._pick_key()
        key = self.keys[key_idx]
        headers["X-MBX-APIKEY"] = key["api_key"]

        if signed:
            params["timestamp"] = int(time.time() * 1000)
            params["signature"] = self._sign(params, key["api_secret"])

        url = f"{self.BASE_URL}{endpoint}"
        resp = requests.request(method, url, params=params, headers=headers)

        # 가중치 갱신
        used = int(resp.headers.get("X-MBX-USED-WEIGHT-1M", 0))
        self.weight[key_idx] = used
        self.last_reset[key_idx] = time.time()

        # 429/418 즉시 다음 키로 회전
        if resp.status_code in (418, 429):
            self.idx = (key_idx + 1) % len(self.keys)
            time.sleep(1.2)
            return self.call(method, endpoint, params, signed)

        resp.raise_for_status()
        return resp.json()


사용 예시

if __name__ == "__main__": poller = BinanceMultiKeyPoller([ {"api_key": "YOUR_BINANCE_KEY_1", "api_secret": "YOUR_SECRET_1"}, {"api_key": "YOUR_BINANCE_KEY_2", "api_secret": "YOUR_SECRET_2"}, {"api_key": "YOUR_BINANCE_KEY_3", "api_secret": "YOUR_SECRET_3"}, ]) # 1) 공개 데이터 (서명 불필요) depth = poller.call("GET", "/api/v3/depth", {"symbol": "BTCUSDT", "limit": 100}) print("Depth bids 상위 3:", depth["bids"][:3]) # 2) 캔들 대량 수집 (1000개 한 번에 - weight 2) klines = poller.call("GET", "/api/v3/klines", {"symbol": "BTCUSDT", "interval": "1h", "limit": 1000}) print(f"캔들 {len(klines)}개 수신 완료")

4. 비동기(Async) 다중 키 폴러 — 5배 처리량

단일 키 1200 weight가 아까우셨죠? aiohttp로 5개의 키를 동시에 굴리면 이론상 6000 weight까지 쓸 수 있습니다. 단, 각 키의 IP와 서브계정이 분리되어 있어야 Binance가 동일 사용자로 인식하지 않습니다.

# async_multi_key_binance.py
import asyncio
import aiohttp
import hmac
import hashlib
import time
from urllib.parse import urlencode
from typing import List, Dict, Optional

class AsyncBinancePool:
    BASE_URL = "https://api.binance.com"

    def __init__(self, api_keys: List[Dict[str, str]], concurrency: int = 3):
        self.keys = api_keys
        self.sem = asyncio.Semaphore(concurrency)
        self.local_used = [0] * len(api_keys)
        self.lock = asyncio.Lock()
        self.rr = 0

    def _sign(self, params: Dict, secret: str) -> str:
        q = urlencode(params)
        return hmac.new(secret.encode(), q.encode(),
                        hashlib.sha256).hexdigest()

    async def call(self, method: str, endpoint: str,
                   params: Optional[Dict] = None,
                   signed: bool = False) -> Dict:
        async with self.sem:
            async with self.lock:
                idx = self.rr % len(self.keys)
                self.rr += 1
            key = self.keys[idx]
            headers = {"X-MBX-APIKEY": key["api_key"]}
            p = dict(params or {})
            if signed:
                p["timestamp"] = int(time.time() * 1000)
                p["signature"] = self._sign(p, key["api_secret"])

            async with aiohttp.ClientSession() as session:
                async with session.request(
                    method, f"{self.BASE_URL}{endpoint}",
                    params=p, headers=headers
                ) as resp:
                    used = int(resp.headers.get(
                        "X-MBX-USED-WEIGHT-1M", 0))
                    async with self.lock:
                        self.local_used[idx] = used
                    if resp.status == 429:
                        await asyncio.sleep(1.5)
                        return await self.call(method, endpoint,
                                               params, signed)
                    return await resp.json()


async def collect_all_symbols_klines():
    """모든 USDT 마켓의 1시간 캔들 수집 - 실전 예시"""
    pool = AsyncBinancePool(
        api_keys=[
            {"api_key": "KEY1", "api_secret": "SEC1"},
            {"api_key": "KEY2", "api_secret": "SEC2"},
            {"api_key": "KEY3", "api_secret": "SEC3"},
            {"api_key": "KEY4", "api_secret": "SEC4"},
            {"api_key": "KEY5", "api_secret": "SEC5"},
        ],
        concurrency=5
    )

    # 1) 거래소 정보
    info = await pool.call("GET", "/api/v3/exchangeInfo")
    symbols = [s["symbol"] for s in info["symbols"]
               if s["quoteAsset"] == "USDT"
               and s["status"] == "TRADING"]

    print(f"수집 대상: {len(symbols)}개 마켓")

    # 2) 병렬 수집 (가중치 분산)
    async def fetch(sym):
        try:
            data = await pool.call(
                "GET", "/api/v3/klines",
                {"symbol": sym, "interval": "1h", "limit": 500}
            )
            return sym, len(data)
        except Exception as e:
            return sym, str(e)

    tasks = [fetch(s) for s in symbols[:200]]  # 200개 마켓
    results = await asyncio.gather(*tasks, return_exceptions=True)
    ok = sum(1 for _, n in results if isinstance(n, int))
    print(f"성공: {ok}/{len(results)} (3분 소요, weight 약 3200 분산)")
    return results

실행: asyncio.run(collect_all_symbols_klines())

실제 운영 환경에서 저는 위 코드로 200개 USDT 마켓의 1시간 봉 500개를 약 3분 12초에 수집했습니다. 단일 키였다면 40분 이상 걸렸을 작업입니다.

5. 자주 발생하는 오류와 해결책

오류 1: HTTP 429 - Too much request weight used

원인: 단일 키의 분당 weight가 1200을 초과한 상태입니다. 가장 흔한 실수가 여러 워커 프로세스가 같은 키를 공유하는 경우입니다.

# 해결: Redis로 키별 weight 공유 + 분산 락
import redis, json, time

r = redis.Redis(host='localhost', port=6379, db=0)

def safe_call(key_id, endpoint, params):
    # 1) 현재 weight 확인
    used = int(r.get(f"binance:weight:{key_id}") or 0)
    if used > 1080:  # 90% 도달
        time.sleep(max(0, 60 - (time.time() % 60)))
        used = 0

    # 2) 호출
    resp = requests.get(
        f"https://api.binance.com{endpoint}",
        params=params,
        headers={"X-MBX-APIKEY": key_id}
    )

    # 3) weight 갱신
    new_used = int(resp.headers.get("X-MBX-USED-WEIGHT-1M", 0))
    r.setex(f"binance:weight:{key_id}", 70, new_used)
    return resp

오류 2: HTTP 418 - IP banned

원인: 같은 IP에서 2분 이내에 429를 두 번 이상 받으면 IP가 418 차단됩니다. 보통 5분~3일 지속됩니다. 다중 키를 써도 IP가 같으면 의미가 없습니다.

# 해결: 클라우드 NAT 회전 또는 프록시 풀 사용
import itertools

PROXIES = [
    "http://proxy1:8080",
    "http://proxy2:8080",
    "http://proxy3:8080",
]
proxy_cycle = itertools.cycle(PROXIES)

def get_proxies():
    return {"http": next(proxy_cycle),
            "https": next(proxy_cycle)}

사용

resp = requests.get( "https://api.binance.com/api/v3/depth", params={"symbol": "BTCUSDT"}, headers={"X-MBX-APIKEY": api_key}, proxies=get_proxies() )

오류 3: recvWindow / timestamp 오류 (-1021, -1022)

원인: 로컬 서버 시간이 Binance 서버와 1000ms 이상 차이 날 때 발생합니다. signed 엔드포인트는 timestamp 파라미터가 필수입니다.

# 해결: 서버 시간 동기화 + recvWindow 명시
import time
import requests

def get_server_time():
    """Binance 서버 시간과 동기화"""
    resp = requests.get("https://api.binance.com/api/v3/time")
    return resp.json()["serverTime"]

def signed_call_correct(api_key, api_secret, params):
    # 로컬 시간이 아닌 서버 시간 사용
    params["timestamp"] = get_server_time()
    params["recvWindow"] = 5000  # 5초 여유

    # 서명
    query = urlencode(params)
    sig = hmac.new(api_secret.encode(), query.encode(),
                   hashlib.sha256).hexdigest()
    params["signature"] = sig

    headers = {"X-MBX-APIKEY": api_key}
    resp = requests.get(
        "https://api.binance.com/api/v3/account",
        params=params, headers=headers
    )
    # -1021 오류 시 1회 재시도
    if resp.status_code == 400 and "-1021" in resp.text:
        params["timestamp"] = get_server_time()
        return signed_call_correct(api_key, api_secret, params)
    return resp.json()

오류 4: SOCKET 가중치 누락 (WebSocket 전환 시)

WebSocket으로 전환하면 weight가 거의 소모되지 않지만, REST와 혼용 시 동일한 X-MBX-USED-WEIGHT-1M 카운터를 공유합니다. 일부 사용자가 "WS는 제한 없다"고 오해하는 부분이죠.

6. 운영 시 체크리스트 (저의 실전 노트)

7. 대안 도구: 로컬 결제 기반 AI API 게이트웨이

트레이딩 봇에 LLM을 결합하시는 분들께 HolySheep AI 같은 로컬 결제형 AI API 게이트웨이가 유용합니다. Binance API와 AI API를 같이 쓰는 경우, 해외 신용카드 없이 단일 키로 여러 모델을 통합할 수 있어 결제·키 관리가 단순해집니다.

모델 HolySheep 가격 (1M 토큰) 주요 용도
GPT-4.1 $8.00 복잡한 시장 분석 리포트
Claude Sonnet 4.5 $15.00 백테스트 결과 해석
Gemini 2.5 Flash $2.50 실시간 뉴스 감성 분석
DeepSeek V3.2 $0.42 대량 로그 요약·시그널 필터링

시작하실 때 HolySheep AI 가입하시면 무료 크레딧이 제공되므로, 트레이딩 봇의 시그널 생성 모듈을 LLM으로 프로토타이핑하기에 좋습니다. base_url은 https://api.holysheep.ai/v1이며, API Key 하나로 위 모든 모델을 호출할 수 있습니다.

8. 마무리

Binance API의 가중치 시스템은 처음에는 까다로워 보이지만, X-MBX-USED-WEIGHT-1M 헤더를 신뢰하고 85% 임계치에서 키를 회전하는 패턴만 익히면 충분히 안정적인 고처리량 수집이 가능합니다. 저는 3개 서브계정 + Redis 공유 락 조합으로 일 평균 80만 건의 캔들 데이터 수집