내 봇이 '너무 빨리' 주문하면 생기는 일 — API 호출 제한 설계
X-MBX-USED-WEIGHT-1M·Remaining-Req)로 잔량을 읽고 ② 계정 단위 공유 토큰 버킷으로 속도를 깎고 ③ Retry-After를 그대로 따르는 백오프를 두고 ④ 시세는 폴링 대신 WebSocket으로 옮깁니다.
자동매매 봇이 멈추는 이유 중 가장 허무한 것이 "너무 부지런해서"입니다. 전략은 멀쩡한데, 시세를 1초에 서른 번 조회하다가 거래소가 문을 닫아버리는 것이죠. KIS API에서는 EGW00201(초당 거래건수 초과), 바이낸스에서는 429, 업비트에서는 Remaining-Req의 sec 값이 0으로 떨어지는 형태로 나타납니다.
이 글은 에러가 뜬 다음에 고치는 법이 아니라, 처음부터 한도 안쪽에서 동작하도록 짜는 법을 다룹니다. 이미 에러 메시지를 받아 든 상태라면 → KIS API 에러코드 11가지 — EGW00201·토큰 만료 해결을 먼저 보세요. 이 글은 그 앞 단계입니다.
이 글의 순서
- 한도는 하나가 아니다 — 요청 수 / 가중치 / 주문 수
- 거래소·증권사별 실측 한도 한 표
- 응답 헤더로 잔량을 읽는다
- 429와 418의 차이 — 백오프를 안 하면 밴
- 토큰 버킷: 그룹별로 속도를 깎는다
- 전략이 늘면 호출량은 조용히 배가 된다
- 폴링을 WebSocket으로 — 근본 처방
- 설계 체크리스트 8개
1. 한도는 하나가 아니다
"초당 몇 번"이라는 한 줄로 외우면 반드시 어딘가에서 막힙니다. 실제로는 서로 독립적인 세 개의 카운터가 동시에 돌아갑니다.
| 카운터 | 세는 대상 | 대표 예 |
|---|---|---|
| 요청 수 (raw requests) | 엔드포인트 종류와 무관하게 호출 횟수 그 자체 | 바이낸스 RAW_REQUESTS, 업비트 그룹별 초당 한도 |
| 가중치 (weight) | 무거운 조회는 더 많이 차감. 같은 1회라도 비용이 다름 | 바이낸스 REQUEST_WEIGHT |
| 주문 수 (orders) | 주문 제출만 따로 셈. 조회와 별도 계정 단위 | 바이낸스 ORDERS, 업비트 exchange.order |
특히 가중치 개념이 낯설어서 사고가 납니다. 바이낸스는 엔드포인트마다 weight가 다르게 매겨져 있어, 전 종목 티커를 한 번 긁는 호출 1회가 단일 종목 조회 수십 회에 해당할 수 있습니다. "호출은 몇 번 안 했는데 왜 막히지?"의 정체가 대개 이것입니다.
2. 거래소·증권사별 한도 한 표
아래는 각 사 공식 문서에서 확인한 값입니다(2026-07-30 기준). 한도 정책은 공지 없이 조정되는 경우가 있으니, 코드에 숫자를 박기 전에 반드시 공식 문서를 다시 확인하세요.
| 대상 | 확인 가능한 한도 | 잔량 확인 방법 |
|---|---|---|
| 바이낸스 (Binance Spot) | REQUEST_WEIGHT · ORDERS · RAW_REQUESTS 세 종류. 실제 수치는 /api/v3/exchangeInfo의 rateLimits 배열에 담겨 옴 |
X-MBX-USED-WEIGHT-1MX-MBX-ORDER-COUNT-10S 등 응답 헤더 |
| 업비트 (Upbit) | 시세(market·candle·trade·ticker·orderbook) 초당 10회(IP) / exchange.default 초당 30회 / exchange.order 초당 8회 / exchange.order-cancel-all 2초당 1회 / websocket-connect 초당 5회 / websocket-message 초당 5회·분당 100회 |
Remaining-Req 헤더group=default; min=1800; sec=29 |
| 빗썸 (Bithumb) | Public API·Private API에 초당 상한이 있고, 주문 관련은 초당 10회 초과 시 제한될 수 있다고 안내 | 공식 문서에 헤더 규격 명시 없음 → 자체 카운터 필요 |
| KIS (한국투자증권) | 초당 거래건수 제한이 있으며, 초과 시 msg_cd에 EGW00201. 실전과 모의투자의 한도가 다름 |
응답의 rt_cd·msg_cd로 사후 감지 |
| 키움 (REST API) | TR(api-id) 단위로 호출 제한. 종류가 다른 TR을 섞으면 합산 처리량은 더 나옴 |
공식 API 명세서 확인 필요 |
⚠️ 숫자를 하드코딩하지 마세요: 바이낸스는 현재 적용 한도를 exchangeInfo로 알려주고, 업비트는 Remaining-Req로 잔량을 알려줍니다. 서버가 말해주는 값을 읽는 코드가, 문서를 보고 상수로 박아둔 코드보다 오래 삽니다. KIS·키움처럼 헤더로 알려주지 않는 쪽은 보수적인 마진을 두는 수밖에 없습니다.
3. 응답 헤더로 잔량을 읽는다
가장 먼저 넣어야 할 코드는 리미터가 아니라 계기판입니다. 지금 얼마나 쓰고 있는지 로그에 안 남으면, 막힌 뒤에도 원인을 모릅니다.
import logging
def log_quota(res, venue):
"""응답 헤더에서 잔량을 뽑아 로그에 남긴다"""
if venue == "binance":
used = res.headers.get("X-MBX-USED-WEIGHT-1M")
orders = res.headers.get("X-MBX-ORDER-COUNT-10S")
logging.info("binance weight_1m=%s order_10s=%s", used, orders)
elif venue == "upbit":
# Remaining-Req: group=default; min=1800; sec=29
raw = res.headers.get("Remaining-Req", "")
parts = dict(p.strip().split("=") for p in raw.split(";") if "=" in p)
logging.info("upbit group=%s sec=%s min=%s",
parts.get("group"), parts.get("sec"), parts.get("min"))
if int(parts.get("sec", 99)) <= 2:
logging.warning("업비트 잔여 요청 %s건 — 감속 필요", parts.get("sec"))
업비트 문서 기준 sec가 0으로 반환되면 잔여 요청이 없는 상태이므로 일정 시간 뒤에 다시 요청해야 합니다. 즉 0을 보고 나서 대응하면 이미 늦고, 2~3 남았을 때 감속해야 합니다.
4. 429와 418 — 백오프를 안 하면 밴
바이낸스 공식 문서의 서술이 명확합니다. 429는 "속도를 줄이라"는 신호이고, 429를 받고도 물러서지 않으면 418로 IP가 자동 차단됩니다. 차단 기간은 반복 위반에 따라 2분에서 3일까지 늘어납니다. 업비트도 429(초당 한도 초과)와 418(누적 위반 일시 차단)을 같은 구조로 씁니다.
그리고 두 코드 모두 Retry-After 헤더가 함께 옵니다. 재시도 간격을 우리가 정하면 안 됩니다. 서버가 알려준 초를 그대로 기다려야 합니다.
import time, requests
def call_with_backoff(fn, max_retry=5):
"""429/418을 Retry-After 기준으로 정직하게 기다린다"""
for attempt in range(max_retry):
res = fn()
if res.status_code not in (429, 418):
return res
wait = int(res.headers.get("Retry-After", 0)) or (2 ** attempt)
if res.status_code == 418:
logging.error("IP 차단(418). %s초 대기 — 요청을 즉시 멈춘다", wait)
else:
logging.warning("429 수신. %s초 감속", wait)
time.sleep(wait)
raise RuntimeError("레이트리밋 재시도 소진 — 봇을 정지하고 원인을 본다")
⚠️ 절대 하면 안 되는 것: 429가 떴다고 try/except로 삼키고 즉시 같은 요청을 다시 던지는 코드. 이것이 바로 418을 부르는 패턴입니다. 특히 while True 안에서 재시도하면 IP 차단 기간이 계단식으로 늘어나, 장중에 봇 전체가 몇 시간씩 죽습니다. 봇이 멈춘 뒤의 복구 절차는 → 자동매매 봇이 멈췄을 때 — 장애·복구 플레이북.
5. 토큰 버킷 — 그룹별로 속도를 깎는다
time.sleep(0.2)를 호출 사이에 흩뿌리는 방식은 두 가지 이유로 무너집니다. ① 전략이 두 개가 되면 각자 0.2초를 지켜도 합산은 두 배가 되고, ② 조회와 주문의 한도가 다른데 같은 간격을 씁니다.
해법은 그룹별 토큰 버킷을 하나씩 두고, 봇의 모든 호출이 그 버킷을 통과하게 만드는 것입니다. 버킷은 초당 정해진 개수만큼 토큰이 채워지고, 호출은 토큰을 하나 써야 나갈 수 있습니다.
import threading, time
class TokenBucket:
"""초당 rate개씩 채워지는 버킷. 스레드 안전."""
def __init__(self, rate, capacity=None):
self.rate = rate
self.capacity = capacity or rate
self.tokens = self.capacity
self.updated = time.monotonic()
self.lock = threading.Lock()
def take(self, n=1):
with self.lock:
while True:
now = time.monotonic()
self.tokens = min(self.capacity,
self.tokens + (now - self.updated) * self.rate)
self.updated = now
if self.tokens >= n:
self.tokens -= n
return
time.sleep((n - self.tokens) / self.rate)
# 업비트 공식 한도의 70% 수준으로 마진을 둔다
BUCKETS = {
"quotation": TokenBucket(7), # 시세 초당 10 → 7
"exchange": TokenBucket(21), # exchange.default 초당 30 → 21
"order": TokenBucket(5), # exchange.order 초당 8 → 5
}
def upbit_get(path, group="quotation", **kw):
BUCKETS[group].take()
return requests.get(f"https://api.upbit.com/v1/{path}", **kw)
마진을 왜 두나요? 서버와 우리 쪽 시계가 완전히 같지 않고, 네트워크 재전송으로 실제 도착 횟수가 우리가 센 것보다 많을 수 있습니다. 한도의 70% 안쪽에서 돌리면 대부분의 경계 사고가 사라집니다. 체결 속도가 아쉬워 보여도, 418로 몇 시간 죽는 것보다 훨씬 낫습니다. 호가 대비 실제 체결이 얼마나 밀리는지는 → 자동매매 주문 체결과 슬리피지에서 따로 다뤘습니다.
6. 전략이 늘면 호출량은 조용히 배가 된다
이 문제가 실제로 가장 자주 터집니다. 처음엔 전략 하나로 시작해 잘 돌던 봇이, 전략을 셋으로 늘리자 EGW00201이 쏟아지는 식입니다. 각 전략이 자기 몫의 리미터를 따로 들고 있으면, 계정 전체 호출량은 세 배가 됩니다.
| 구성 | 전략별 리미터 | 실제 계정 호출량 | 결과 |
|---|---|---|---|
| 전략 1개 | 초당 5 | 초당 5 | 정상 |
| 전략 3개 | 각 초당 5 | 초당 15 | 한도 초과 |
| 전략 3개 + 공유 버킷 | 공유 초당 5 | 초당 5 | 정상(전략끼리 순번 대기) |
리미터는 전략이 아니라 자격증명(계정·IP) 단위로 존재해야 합니다. 프로세스를 여러 개 띄워 전략을 분리했다면 프로세스마다 버킷이 생기므로, 그때는 주문·조회를 한 게이트웨이 프로세스로 모으거나 Redis 같은 공용 저장소로 토큰을 공유해야 합니다. 여러 전략을 한 계좌에서 굴릴 때의 설계는 → 전략 여러 개를 한 계좌에서 돌릴 때를 참고하세요.
💡 종목 수는 호출량의 곱셈 인자입니다: 10종목을 1초마다 개별 조회하면 초당 10건입니다. 30종목이면 30건. 그래서 관심종목을 늘리는 순간 봇이 막히는 것이고, 대부분의 거래소가 다중 종목을 한 번에 받는 엔드포인트를 제공하는 이유이기도 합니다(업비트 /v1/ticker?markets=에 쉼표로 나열). 단, 바이낸스처럼 전체 조회에 큰 weight를 매기는 곳도 있으니 가중치까지 함께 보셔야 합니다.
7. 폴링을 WebSocket으로 — 근본 처방
리미터는 증상 관리이고, 호출량 자체를 줄이는 방법은 하나뿐입니다. "물어보는" 대신 "받아보는" 구조로 바꾸는 것입니다.
| REST 폴링 | WebSocket 구독 | |
|---|---|---|
| 30종목 시세 | 초당 30건 소모 | 접속 1회 + 구독 메시지 |
| 지연 | 폴링 주기만큼 늦음 | 변경 즉시 수신 |
| 한도 그룹 | 시세·조회 그룹 소모 | 별도 그룹(업비트: websocket-connect 초당 5회, websocket-message 초당 5회·분당 100회) |
| 대신 필요한 것 | — | 재접속·하트비트·구독 복원 로직 |
다만 WebSocket으로 옮기면 끊김 처리라는 새 숙제가 생깁니다. 연결이 죽었는데 봇이 모르면 "가격이 안 변한다"고 착각해 아무 주문도 안 내거나, 반대로 낡은 가격으로 주문을 냅니다. 업비트의 REST·WebSocket 병행 구조는 → 업비트 REST·WebSocket 실전, 키움 쪽 실시간 처리는 → 키움 REST API 자동매매 완전 가이드에 정리돼 있습니다.
무중단 운영 환경에서는 재접속 실패가 곧 거래 중단이므로, 하트비트가 끊긴 시각과 마지막 수신 가격의 타임스탬프를 함께 로그에 남기고, 일정 시간 이상 조용하면 REST 조회로 잠깐 되돌아가는 폴백을 두는 것이 안전합니다.
8. 설계 체크리스트 8개
✅ 배포 전에 확인할 것
- 응답 헤더(
X-MBX-USED-WEIGHT-1M·Remaining-Req)를 로그에 남기고 있는가 - 리미터가 전략별이 아니라 계정·IP 단위로 공유되는가
- 조회·주문·취소 그룹의 버킷이 분리돼 있는가
- 공식 한도의 70% 안쪽에서 도는가
- 429·418에서
Retry-After를 그대로 따르는가 - 재시도 횟수 상한이 있고, 소진되면 봇이 멈추는가(무한 재시도 금지)
- 종목 수를 늘렸을 때 호출량이 어떻게 변하는지 계산해 봤는가
- 시세는 WebSocket으로 옮길 수 있는가
증권사·거래소마다 한도 체계가 이렇게 다르기 때문에, "어디에 붙일 것인가"를 정할 때 수수료뿐 아니라 호출 한도와 실시간 지원 여부를 같이 보셔야 합니다 → 키움·KIS·LS·대신 자동매매 API 비교. 바이낸스에서 실제로 자주 밟는 함정들은 → 바이낸스 봇 제작 시 자주 걸리는 함정에 따로 모아뒀습니다.
같은 429라도 가중치 초과인지·주문 수 초과인지·전략 합산 때문인지에 따라 처방이 완전히 다릅니다. 로그 한 줄만 봐도 갈리는 경우가 많아, 막힌 지점만 짚어드릴 수 있어요.
이 부분만 대신 만들어 드립니다 — 무료 상담 →자주 묻는 질문
Q. 바이낸스 API 제한은 어떻게 확인하나요?
응답 헤더 X-MBX-USED-WEIGHT-1M으로 최근 1분 사용 가중치를, 주문 응답의 X-MBX-ORDER-COUNT-…로 주문 수를 봅니다. 현재 적용된 한도 값 자체는 /api/v3/exchangeInfo의 rateLimits에서 REQUEST_WEIGHT·ORDERS·RAW_REQUESTS로 확인하세요.
Q. 429와 418은 무엇이 다른가요?
429는 한도 초과 경고, 418은 429를 무시하고 계속 던져서 IP가 차단된 상태입니다. 바이낸스 기준 차단은 반복 위반에 따라 2분~3일까지 늘어납니다. 둘 다 Retry-After를 따르세요.
Q. KIS API의 EGW00201은 왜 반복되나요?
초당 거래건수 초과입니다. 종목 수나 전략 수가 늘면서 합산 호출량이 커진 경우가 대부분이라, 전략별 sleep이 아니라 계정 단위 공유 리미터로 바꿔야 합니다. 실전과 모의투자의 한도가 다르니 공식 문서를 확인하세요.
Q. 호출 제한을 근본적으로 줄이려면?
시세 폴링을 WebSocket 구독으로 옮기는 것입니다. 업비트 기준 WebSocket은 REST와 별도 그룹(websocket-connect 초당 5회 등)이라 조회 한도를 잡아먹지 않습니다. 대신 재접속·구독 복원 로직이 필요합니다.
마무리
레이트리밋은 "에러 핸들링" 항목이 아니라 설계 항목입니다. 계기판(헤더 로깅) → 감속기(공유 토큰 버킷) → 안전장치(Retry-After 백오프) → 구조 변경(WebSocket) 순서로 쌓으면, 종목이나 전략을 늘려도 봇이 조용히 죽는 일이 없어집니다.
참고로 이 문제는 증권사·거래소마다 규격이 전부 달라서 한 번 짜두면 끝나는 코드가 아닙니다. 어느 증권사가 무엇을 어디까지 열어주는지부터 확인이 필요하다면 → 키움 해외주식 API 되나 — REST 미국주식 지원 범위도 함께 보세요.
한도 관리·백오프·WebSocket 재접속까지 포함한 안정적인 봇 구조가 필요하시면, 알고랩이 통합 제작해 드립니다.
호출 제한까지 계산된 봇 제작
공유 리미터, Retry-After 백오프, WebSocket 재접속, 다중 전략 합산 호출량 관리까지 — 알고랩이 통합 패키지로 제작합니다.
24시간 빠른 답변 가능합니다.