AlgoLab Blog · 키움 REST API 트러블슈팅 · 2026

키움 REST API 429 — 허용된 요청 개수 초과 해결

키움증권 · 트러블슈팅 2026-08-07 · 약 8분 읽기 · 알고랩 AlgoLab
한 줄 요약 HTTP 429 + return_code: 5 + "허용된 요청 개수를 초과하였습니다"코드 버그가 아니라 호출이 너무 빠르다는 신호입니다. 요청 내용은 정상이므로 api-id나 파라미터를 손댈 필요가 없습니다. 핵심은 두 가지입니다 — ① 키움은 한도가 api-id(TR) 단위로 따로 걸립니다. 한국투자증권이 appkey 단위로 전부 합산하는 것과 정반대라, KIS 대응책을 그대로 옮기면 봇만 느려지고 429는 그대로입니다. ② 그래서 전역 time.sleep()이 아니라 api-id를 키로 하는 리미터를 두고, 뚫리면 지수 백오프 + 지터로 재시도하면 됩니다. 조회 종목이 많아 계속 걸린다면 REST 폴링을 늘리지 말고 WebSocket으로 옮기는 것이 정답입니다.

키움 REST API 자동매매 완전 가이드에서 토큰·TR·WebSocket을 전반적으로 다뤘지만, 429 하나만 검색해서 들어오는 분이 꾸준합니다. 이유는 분명합니다 — 이 오류는 봇이 실제로 돌기 시작한 다음에야 나타나고, 그때는 이미 "어제까지 잘 되던 코드"이기 때문입니다.

이 글은 키움 REST API429 한 건만 다룹니다. 한국투자증권 쪽 EGW00201별도 글에, 애초에 한도 안에서 돌도록 설계하는 원리는 호출 제한 설계에 있습니다.

1. 응답이 어떻게 생겼나

키움 REST API가 한도를 넘겼다고 판단하면 HTTP 상태코드와 본문 양쪽으로 알려 줍니다.

HTTP/1.1 429 Too Many Requests
Content-Type: application/json

{
  "return_code": 5,
  "return_msg": "허용된 요청 개수를 초과하였습니다"
}

정상 응답과 비교하면 판정 지점이 분명해집니다.

{
  "return_code": 0,
  "return_msg": "정상적으로 처리되었습니다"
}

가장 흔한 실수 셋: 429를 보고 ① appkey·secretkey를 재발급받거나 ② api-id가 틀렸나 의심해 문서를 다시 뒤지거나 ③ 접근토큰을 새로 받으러 갑니다. 전부 무관합니다. 특히 토큰 재발급은 그 자체로 호출이라, 429가 났다고 토큰을 다시 받으면 문제를 하나 더 만드는 길입니다. 토큰은 expires_dt 기준으로 캐싱해 두고 만료 전까지 재사용하세요.

2. 왜 HTTP 상태코드만 보면 안 되나

키움 REST API는 응답 본문에 return_code·return_msg를 담아 결과를 알려 줍니다. 여기서 실무자가 반드시 지켜야 할 원칙이 나옵니다 — HTTP 상태코드와 return_code를 둘 다 검사해야 합니다.

429는 상태코드로도 드러나는 친절한 경우입니다. 문제는 주문이 거부됐는데 HTTP는 200으로 내려오는 상황입니다. 상태코드만 보고 raise_for_status() 하나로 끝내면, 주문이 안 나간 것을 봇이 모른 채 다음 로직으로 넘어갑니다.

import requests

def call(api_id: str, path: str, body: dict, token: str):
    r = requests.post(
        f"https://api.kiwoom.com{path}",
        headers={
            "authorization": f"Bearer {token}",
            "api-id": api_id,              # 기능 구분 — 정확한 코드는 공식 문서 확인
            "Content-Type": "application/json;charset=UTF-8",
        },
        json=body, timeout=5,
    )

    # ① 속도 문제 — 재시도로 넘길 수 있는 오류
    if r.status_code == 429:
        raise RateLimited(api_id)

    # ② HTTP 200이어도 실패일 수 있다
    data = r.json()
    if data.get("return_code") != 0:
        raise KiwoomError(data.get("return_code"), data.get("return_msg"))

    return data

여기서 429를 별도 예외로 분리한 것이 핵심입니다. 429기다리면 풀리는 오류고, 나머지 return_code 오류는 대개 사람이 손대야 끝나는 오류입니다. 이 둘을 같은 except로 묶어 재시도를 돌리면 잘못된 주문을 무한 반복하게 됩니다.

3. 한도가 걸리는 단위 — 키움과 KIS가 정반대다

이 오류를 오래 못 잡는 분들의 공통점은 KIS에서 쓰던 대응을 그대로 옮긴 경우입니다. 두 증권사는 한도가 걸리는 단위 자체가 다릅니다.

구분한국투자증권(KIS)키움증권 REST API
한도 단위appkey 단위로 전부 합산api-id(TR) 단위로 각각 별도
오류 신호HTTP 200 + msg_cd: EGW00201HTTP 429 + return_code: 5
프로세스를 나누면같은 앱키면 효과 없음쓰는 TR이 다르면 간섭이 적음
전역 sleep의 효과어느 정도 통함봇만 느려짐
맞는 처방앱키 단위 전역 리미터 1개api-id별 리미터 여러 개
KIS — appkey 하나에 전부 합산 주문 잔고조회 시세조회 appkey 한도 하나를 나눠 씀 → 프로세스를 나눠도 소용없다 키움 — api-id마다 별도 한도 주문 TR 잔고 TR 시세 TR 버킷 A 버킷 B 버킷 C → 시세가 막혀도 주문은 나간다 ※ 정확한 허용치는 엔드포인트마다 다를 수 있음 — 공식 개발자 문서 확인
같은 "호출 제한"이라도 걸리는 단위가 다르면 처방이 달라진다

4. 어느 정도 속도가 안전한가

공개된 실측 사례에서는 키움 REST APITR 하나당 초당 1회 정도가 안전한 지속 속도이고, 순간적으로 2회 정도까지 버스트가 허용되는 수준으로 보고되고 있습니다. 레거시 OpenAPI+ 시절 흔히 권장되던 초당 약 5회와는 감각이 다릅니다.

이 숫자를 코드에 하드코딩하지 마세요. 위 값은 커뮤니티·라이브러리 구현에서 관찰된 보수적 기준이고, 정확한 허용치는 엔드포인트마다 다를 수 있습니다. 키움증권 공식 개발자 문서의 값을 확인해 설정 파일로 빼 두고, 거기서 다시 20~30% 낮게 잡는 편이 안전합니다. 정책은 예고 없이 바뀔 수 있습니다.

5. 처방 — api-id별 토큰 버킷

한도가 TR별로 나뉘어 있으니, 리미터도 api-id를 키로 하는 딕셔너리로 두는 것이 자연스럽습니다. 아래는 스레드 안전한 토큰 버킷 구현입니다.

import time, threading
from collections import defaultdict

class TokenBucket:
    """초당 rate개를 채우고 최대 burst개까지 모아 두는 버킷"""
    def __init__(self, rate=1.0, burst=2):
        self.rate, self.burst = rate, burst
        self.tokens = float(burst)
        self.updated = time.monotonic()
        self.lock = threading.Lock()

    def acquire(self):
        with self.lock:
            now = time.monotonic()
            self.tokens = min(self.burst,
                              self.tokens + (now - self.updated) * self.rate)
            self.updated = now
            if self.tokens < 1.0:
                wait = (1.0 - self.tokens) / self.rate
            else:
                self.tokens -= 1.0
                return
        time.sleep(wait)          # 락 밖에서 대기 — 다른 TR을 막지 않는다
        self.acquire()

# api-id마다 버킷을 따로 — 시세 조회가 주문 경로를 붙잡지 않는다
_buckets = defaultdict(lambda: TokenBucket(rate=1.0, burst=2))

def throttled(api_id: str):
    _buckets[api_id].acquire()

이제 모든 호출이 자기 api-id의 버킷만 소비합니다. 시세 조회가 몰려 대기가 걸려도 주문 TR은 그대로 즉시 통과합니다. 자동매매에서 주문 지연은 곧 슬리피지 비용이라, 이 분리가 실제 손익에 영향을 줍니다.

6. 그래도 뚫리면 — 백오프 + 지터

리미터를 넣어도 429는 완전히 사라지지 않습니다. 네트워크 지연과 서버 측 계측 시점이 어긋나기 때문입니다. 재시도는 반드시 간격을 흩뜨려서 해야 합니다.

import random

class RateLimited(Exception): ...
class KiwoomError(Exception): ...

def with_retry(fn, *args, tries=5, base=0.5, **kw):
    for i in range(tries):
        try:
            return fn(*args, **kw)
        except RateLimited:
            if i == tries - 1:
                raise
            # 지수 백오프 + 지터 — 막힌 요청들이 같은 시점에 다시 몰리는 것을 막는다
            time.sleep(base * (2 ** i) + random.uniform(0, 0.3))
        # KiwoomError는 잡지 않는다 — 사람이 손대야 끝나는 오류다

지터를 빼면 안 되는 이유: 고정 간격으로 재시도하면 동시에 막혔던 요청들이 정확히 같은 시점에 다시 몰립니다. 한 번 429가 나면 그 뒤로 계속 429가 나는 동기화 현상이 이렇게 생깁니다. 0~0.3초 정도의 무작위 값만 섞어도 눈에 띄게 줄어듭니다.

7. 조회가 많아서 걸리는 경우 — WebSocket으로 옮기기

리미터를 제대로 넣었는데도 계속 걸린다면, 대개 호출 구조 자체가 한도를 넘게 설계돼 있습니다. 전형적인 형태가 종목별 REST 폴링입니다.

구조감시 종목 30개일 때결과
REST로 1초마다 종목별 조회초당 30회 호출429 확정
REST로 5초마다 종목별 조회초당 6회 + 신호 5초 지연느려지고 여전히 위험
WebSocket 실시간 등록등록은 한 번, 이후 수신호출 수 문제에서 벗어남

키움 REST API는 실시간 시세를 WebSocket으로 제공합니다. 한 번 등록해 두면 여러 종목의 시세가 밀려 들어오는 방식이라 종목 수가 곧 호출 수가 되는 구조에서 벗어날 수 있습니다. 다만 세션당 등록 가능한 종목 수에도 제한이 있을 수 있으므로 정확한 값은 공식 문서에서 확인한 뒤 설계하세요. 연결 방식과 재접속 처리는 키움 REST API 완전 가이드에 정리돼 있습니다.

💬
리미터를 넣었더니 이번엔 봇이 너무 느려졌다면

감시 종목 수·전략 주기·조회 항목을 놓고 어디를 WebSocket으로 옮기고 어디를 줄일지 함께 설계해 드립니다.

→ 무료 상담으로 호출 구조 점검받기

정리 — 진단 순서

단계할 일
1HTTP 상태코드와 return_code둘 다 검사한다
2429·return_code 5재시도 가능으로 별도 분류한다
3전역 sleep을 걷어내고 api-id별 토큰 버킷을 넣는다
4재시도에 지수 백오프 + 지터를 붙이고 5회에서 끊는다
5그래도 걸리면 종목별 폴링을 WebSocket으로 옮긴다
6한도 값은 설정으로 분리 — 공식 문서 값에서 20~30% 낮게

증권사마다 한도 체계와 오류 규격이 다릅니다. 한국투자증권·LS증권·대신증권과의 차이는 증권사 API 비교에서, 두 증권사 API의 구조적 차이는 키움 vs KIS 비교에서 확인하실 수 있습니다. API를 쓸 때 실제로 나가는 돈은 증권사 API 수수료에 정리했습니다.

자주 묻는 질문

프로세스를 나누면 한도가 늘어나나요?

키움은 한도가 api-id 단위로 걸리므로 서로 다른 TR을 쓰는 프로세스끼리는 간섭이 적습니다. 다만 같은 TR을 두 프로세스가 동시에 부르면 그 TR의 한도는 여전히 공유됩니다. 백테스트용 수집 스크립트와 실매매 봇이 같은 시세 TR을 쓰고 있는지부터 확인해 보세요.

KIS에서 쓰던 전역 리미터를 그대로 옮겨도 되나요?

동작은 하지만 손해입니다. 전역 리미터는 모든 호출을 한 줄로 세우기 때문에, TR별로 한도가 나뉘어 있는 키움에서는 쓰지 않아도 될 여유까지 버리는 셈이 됩니다. 시세 조회가 주문을 붙잡아 체결이 늦어지는 것이 대표적인 부작용입니다.

return_code가 5가 아닌 다른 값일 때도 재시도하면 되나요?

안 됩니다. 429·return_code 5는 "속도" 문제라 시간이 해결하지만, 토큰 만료·권한·잘못된 종목코드·주문 조건 오류처럼 사람이 손대야 끝나는 오류를 재시도로 돌리면 무한 루프가 됩니다. 재시도 가능 목록을 상수로 명시해 두고 그 밖은 즉시 예외로 올리세요.

토큰을 자주 재발급받는 것도 429의 원인이 되나요?

됩니다. 토큰 발급도 호출이기 때문에 매 요청마다 새로 받으면 그 자체로 제한에 걸립니다. 접근토큰은 expires_dt 기준으로 캐싱해 만료 직전에만 갱신하는 것이 표준입니다. 만료 시각을 신뢰하지 못하겠다면 만료 60초 전 갱신 정도의 여유를 두세요.

키움증권 자동매매 봇 맞춤 제작

호출 한도 설계·재시도 정책·WebSocket 전환·24시간 무중단 운영까지 — 알고랩이 통합 패키지로 제작합니다.
24시간 빠른 답변 가능합니다.

무료 상담 시작하기