지난봄, 포지션 진입 신호가 들어왔는데 주문이 체결되지 않았습니다. 로그를 보니 HTTP 429 응답이었습니다. rate limit 초과였죠. 처음엔 단순 버그라 생각했지만 두 번째 같은 상황이 반복되고 나서야 이게 운영 구조의 문제라는 걸 인정했습니다. 바이낸스 API는 생각보다 엄격하고, 멀티 페어 운영 환경에서는 특히 쉽게 한계에 닿습니다. 이번 글은 제가 실제로 겪고 수정한 내용을 공유합니다.
rate limit이 왜 문제가 되는지부터 정확히 이해해야 합니다
바이낸스 API에는 크게 두 가지 제한이 있습니다. 첫째는 요청 가중치(weight) 기반 제한이고, 둘째는 주문 횟수 제한입니다. 많은 분들이 단순히 ‘초당 몇 번’이라고만 생각하는데, 실제로는 엔드포인트마다 소비하는 weight가 다릅니다. 예를 들어 /fapi/v1/order는 weight 1이지만, /fapi/v1/allOrders는 weight 5 이상입니다. 1분 기준 총 weight 한도는 선물 기준 2,400입니다. 멀티 페어 운영 시 5개 페어가 동시에 포지션 확인 + 잔고 조회 + 주문 요청을 하면, 단 몇 초 만에 수백 weight를 소모합니다. 제가 처음 429를 맞은 원인이 바로 여기였습니다. BTC, ETH, SOL, BNB, XRP 5개 페어가 4시간봉 마감 시점에 동시에 깨어나서 일제히 API를 호출했고, 그 순간 weight가 폭발했습니다. 문제를 정확히 이해하고 나면 해결책도 명확해집니다. 단순히 요청 수를 줄이는 게 아니라, 요청이 몰리는 타이밍을 분산시키고 weight를 절약하는 호출 구조로 바꿔야 합니다.
실제로 적용한 세 가지 대응 구조
첫 번째는 페어별 호출 지연(stagger) 입니다. 5개 페어가 동시에 실행되지 않도록, 각 페어 루프 시작 전에 time.sleep(0.3)을 추가했습니다. 0.3초씩만 밀어도 5개 페어의 초기 호출이 1.2초에 걸쳐 퍼집니다. 단순하지만 429 빈도가 눈에 띄게 줄었습니다. 두 번째는 캐싱입니다. 잔고 조회(/fapi/v2/balance)는 모든 페어가 공통으로 사용하는데, 각 페어가 독립적으로 호출하면 5배 낭비입니다. 이를 하나의 공유 객체에 저장하고 10초 TTL로 캐싱했습니다. 코드로는 balance_cache = {} 딕셔너리에 timestamp와 data를 함께 저장하고, 10초 이내면 API 대신 캐시를 반환하는 구조입니다. 세 번째는 지수 백오프(exponential backoff) 재시도 로직입니다. 429가 발생했을 때 즉시 재시도하면 더 빠르게 차단됩니다. 저는 1초, 2초, 4초, 8초 간격으로 최대 4회 재시도하는 함수를 만들고, 429 응답 헤더의 Retry-After 값을 실제로 읽어서 그만큼 대기하도록 했습니다. 바이낸스는 응답 헤더에 X-MBX-USED-WEIGHT-1M 값도 내려줍니다. 이 값을 로그에 찍어두면 현재 소비량을 실시간으로 모니터링할 수 있습니다.
Jesse 프레임워크 + 웹훅 환경에서의 실전 적용
제가 쓰는 환경은 TradingView 웹훅 → Flask API → Jesse 실행 구조입니다. 여기서 rate limit 대응을 어느 레이어에 심을지가 중요합니다. 저는 Flask API의 웹훅 수신 엔드포인트에 간단한 큐(queue) 구조를 추가했습니다. 웹훅이 들어오면 즉시 실행하지 않고 deque에 넣고, 별도 스레드가 0.5초 간격으로 하나씩 꺼내 실행합니다. 이렇게 하면 TradingView에서 여러 알림이 동시에 터져도 API 호출이 직렬화됩니다. 코드 예시는 이렇습니다. from collections import deque와 threading.Thread를 사용해서 signal_queue = deque()에 쌓고, worker 스레드가 while True: if signal_queue: process(signal_queue.popleft()), time.sleep(0.5) 형태로 처리합니다. VPS 환경(Ubuntu + Cafe24)에서는 이 Flask 서버를 systemd 서비스로 등록해서 재시작 시 자동 복구되게 해뒀습니다. 실제로 이 구조를 적용한 뒤, 4시간봉 마감 동시 신호 상황에서도 429 없이 모든 주문이 순차적으로 처리되는 걸 확인하는 중입니다. 아직 모든 극단 상황을 다 검증한 건 아니지만, 방향성은 맞다고 판단하고 있습니다.
💬 운영자 한마디
솔직히 처음 429를 봤을 때 바이낸스 서버 탓을 했습니다. 두 번째 맞고 나서야 제 코드 탓임을 인정했죠. rate limit은 인프라 설계의 문제입니다. 단순히 sleep 하나 추가하는 게 아니라 호출 구조 자체를 다시 생각해야 해결됩니다. 저도 아직 검증 중이고, 더 나은 방법이 있으면 계속 업데이트할 예정입니다.
— J_River · autoprofit 운영자
✅ 이번 주 체크리스트
- 바이낸스 선물 API 문서에서 각 엔드포인트의 weight 값을 직접 확인한다
- 응답 헤더 X-MBX-USED-WEIGHT-1M 값을 로그에 기록하도록 코드를 수정한다
- 멀티 페어 환경이라면 페어별 루프 시작에 time.sleep(0.2~0.5)을 추가한다
- 잔고·포지션 조회처럼 공통 데이터는 캐싱 구조로 중복 호출을 제거한다
- 429 발생 시 즉시 재시도 대신 지수 백오프 재시도 함수를 구현한다
- 웹훅 수신 구조에 큐를 추가해 동시 신호를 직렬 처리로 전환한다
- Flask 서버를 systemd 서비스로 등록해 VPS 재부팅 후에도 자동 복구되게 한다
rate limit은 시스템이 성장할수록 반드시 다시 만나는 벽입니다. 미리 설계해두면 그냥 스쳐지나가는 숫자지만, 대비 없이 만나면 실제 주문이 사라집니다. 지금 멀티 페어 운영 중이라면 한 번쯤 호출 구조를 점검해보시길 권합니다.
※ 본 콘텐츠는 운영자 개인의 경험과 연구를 바탕으로 한 정보 제공 목적이며, 특정 종목에 대한 투자 권유나 매매 시그널이 아닙니다. 암호화폐 및 자동매매는 원금 손실 가능성이 있는 고위험 활동이며, 모든 투자 판단과 그 결과에 대한 책임은 투자자 본인에게 있습니다.