스크립트로 거래소 시세를 받아 보신 분이라면 한 번쯤 겪는 일이 있습니다. 분명히 뭔가 잘못 보냈는데 프로그램은 아무 소리 없이 넘어갑니다.
응답 코드가 200이었기 때문입니다. 오류라고 알려 주는 자리가 상태 코드가 아니라 본문 안에 있었던 것입니다.
그래서 국내 원화 거래소 네 곳의 인증 없이 부를 수 있는 공개 창구에 같은 종류의 잘못된 요청을 똑같이 보내 보고, 무엇이 돌아오는지만 세어 봤습니다.
먼저 이 글이 재는 자리를 못박아 두겠습니다. 인증 헤더도 API 키도 한 번도 붙이지 않고 업비트·빗썸·코빗·코인원의 공개 엔드포인트를 2026년 9월 9일 오전 10시 40분 59초부터 10시 51분 20초 사이에 직접 불러 받은 값입니다. 시각은 전부 한국 기준입니다. 기록된 HTTP 응답은 모두 389건입니다. 시세도 종목도 다루지 않습니다. 매매 권유가 아닙니다.
![]()
앞서 다룬 글과 어디가 다른지부터 갈라 둡니다
거래소 API 호출 제한이라는 말은 이미 한 번 다뤄진 주제입니다. 그래서 이 글이 세는 축이 무엇인지를 먼저 분리해 두겠습니다.
앞선 조사는 해외 거래소 열두 곳을 대상으로 잔여 요청 머리글을 주느냐 주지 않느냐로 갈랐습니다. 그 글에는 국내 원화 거래소가 한 번도 나오지 않습니다.
이 글은 국내 원화 거래소 넷만 봅니다. 그리고 넷은 모두 머리글을 줍니다. 그러니 있느냐 없느냐를 세는 것은 의미가 없습니다. 이 글이 세는 것은 이름과 단위와 상태 코드 관행이 어떻게 갈리는지입니다. 같은 물음에 창구 넷이 각각 다른 모양으로 답하는 자리를 재는 셈입니다.
무엇을 언제 불렀는지부터 적습니다
숫자보다 재료를 먼저 밝혀 두겠습니다. 부른 창구를 거래소별로 옮깁니다.
업비트 쪽입니다.
| 창구 | HTTP | 받은 것 |
|---|---|---|
/v1/market/all?isDetails=true | 200 | 배열 850행 |
/v1/ticker?markets=KRW-BTC | 200 | 배열 1행 |
/v1/orderbook?markets=KRW-BTC | 200 | 배열 1행 |
/v1/trades/ticks?market=KRW-BTC&count=1 | 200 | 배열 1행 |
/v1/candles/ 초·분·일·주·월·연 | 200 | 각 배열 |
/v1/candles/minutes/2 | 400 | 오류 본문 |
/v1/orderbook/instruments?quote_currencies=KRW | 400 | 오류 본문 |
/v1/orderbook/supported_levels | 400 | 오류 본문 (제가 추측해 넣은 경로입니다) |
빗썸은 세대가 둘이라 양쪽을 각각 불렀습니다. 신세대는 경로가 /v1/ 로 시작하고 구세대는 /public/ 로 시작합니다.
| 창구 | HTTP | 받은 것 |
|---|---|---|
/v1/market/all?isDetails=true | 200 | 배열 494행 |
/v1/ticker · /v1/orderbook · /v1/trades/ticks · /v1/candles/days | 200 | 각 배열 |
/public/ticker/ALL_KRW | 200 | data 아래 482키 |
/public/orderbook/BTC_KRW | 200 | data 아래 5키 |
/public/transaction_history/BTC_KRW | 200 | 정상 |
/public/assetsstatus/ALL | 200 | 정상 |
코인원과 코빗 쪽입니다.
| 거래소 | 창구 | HTTP | 받은 것 |
|---|---|---|---|
| 코인원 | /public/v2/markets/KRW | 200 | 마켓 364행 |
| 코인원 | /public/v2/currencies | 200 | 통화 600행 |
| 코인원 | /public/v2/range_units | 200 | 16행 |
| 코인원 | /public/v2/ticker_new/KRW/BTC | 200 | 정상 |
| 코인원 | /public/v2/chart/KRW/BTC?interval=1m | 200 | 2행 |
| 코빗 | /v2/tickers | 200 | data 227행 |
| 코빗 | /v2/currencies | 200 | data 208행 |
| 코빗 | /v2/orderbook?symbol=btc_krw | 200 | data 3키 |
| 코빗 | /v2/candles?symbol=btc_krw&interval=1 | 200 | data 2행 |
필드 이름의 뜻을 인용하기 위해 각 사 문서 페이지도 함께 받았습니다. 여기서 이 글의 가장 큰 한계가 나오므로 먼저 적어 두겠습니다.
| 문서 | HTTP | 원문 HTML 에서 헤더 이름을 찾았나 |
|---|---|---|
| 업비트 한국어 이용 안내 | 200 | 찾음. Remaining-Req 가 9회 등장 |
| 업비트 영어판 같은 문서 | 200 | 못 찾음. 해당 문자열 0회 |
| 코빗 API 문서 | 200 | 찾음. Ratelimit 설명 문단이 있음 |
| 빗썸 API 문서 | 200 | 못 찾음 |
| 코인원 API 문서 | 200 | 못 찾음 |
빗썸과 코인원의 문서 문언은 확인하지 못했습니다. 두 문서 루트는 HTTP 200으로 열리지만 받아 온 HTML 안에 헤더 이름이 한 번도 없습니다. 스크립트로 화면을 그리는 형태라서 원문에 문자열이 담기지 않습니다. 그래서 이 글의 빗썸 절과 코인원 절은 제가 받은 응답에만 근거합니다. 회사가 스스로 뭐라고 적어 두었는지는 모릅니다. 반대로 업비트와 코빗의 문언은 200으로 열린 문서 페이지에서 직접 뽑아 인용한 것입니다.
한 가지 더 눈에 걸립니다. 업비트는 같은 안내 문서의 한국어판에만 헤더 이름이 있었고 영어판에는 없었습니다. 어느 쪽이 최신인지는 이 글이 판정하지 않습니다. 다만 문서를 영어로만 읽는 사람과 한국어로 읽는 사람이 다른 정보를 갖게 되는 자리라는 점은 적어 둡니다.
네 거래소가 같은 이름을 쓰는 헤더는 하나도 없습니다
응답에 실려 온 잔여 요청 관련 헤더를 그대로 옮깁니다. 이름을 제 마음대로 바꿔 부르면 나중에 대조가 되지 않으므로 각 사가 쓴 철자 그대로 적습니다.
| 거래소 | 응답에 실린 헤더 이름 |
|---|---|
| 업비트 | Remaining-Req |
| 빗썸 | X-RateLimit-Remaining · X-RateLimit-Burst-Capacity · X-RateLimit-Replenish-Rate · X-RateLimit-Requested-Tokens |
| 코인원 | Public-Ratelimit-Remaining · Public-Ratelimit-Burst-Capacity · Public-Ratelimit-Replenish-Rate |
| 코빗 | Ratelimit · Ratelimit-Policy |
두 거래소가 함께 쓰는 헤더 이름은 0개입니다. 재검산 스크립트로 네 집합의 교집합을 다시 계산해도 결과는 비어 있었습니다.
이름만 다른 것이 아닙니다. 값을 담는 모양도 넷이 전부 다릅니다.
| 거래소 | 값의 모양 | 담긴 정보 |
|---|---|---|
| 업비트 | group=candles; min=600; sec=9 | 그룹 이름 · 분 필드 · 초 단위 잔여 |
| 빗썸 | 헤더 네 개에 숫자 하나씩 | 잔여 · 버스트 한도 · 보충률 · 이번 요청이 쓴 토큰 |
| 코인원 | 헤더 세 개에 숫자 하나씩 | 잔여 · 버스트 한도 · 보충률 |
| 코빗 | limit=50, remaining=49, reset=1 | 한도 · 잔여 · 초기화까지 남은 초 |
업비트와 코빗은 한 헤더 안에 키와 값을 여러 쌍 넣고, 빗썸과 코인원은 헤더를 여러 개로 쪼갭니다. 즉 파서를 하나 써서 네 곳을 다 읽을 수가 없습니다. 거래소를 추가할 때마다 값을 꺼내는 코드를 따로 써야 한다는 뜻입니다.
같은 질문에 넷이 각각 다르게 답하는 자리는 이 사이트가 앞서 네 거래소의 등락률 기준 시각을 재어 본 글에서도 확인한 바 있습니다. 그때는 숫자의 기준 시점이 갈렸고, 이번에는 그 숫자를 실어 나르는 봉투가 갈립니다.
다섯 갈래 중 셋은 틀린 요청에도 HTTP 200 을 돌려줍니다
이 글의 표제 숫자입니다. 같은 종류의 실수를 다섯 API 표면에 똑같이 보냈습니다. 없는 캔들 단위와 없는 마켓 코드를 넣는 방식입니다.
| API 표면 | 넣은 값 | HTTP | 본문이 알려 준 것 |
|---|---|---|---|
| 업비트 | 없는 캔들 단위 | 400 | error 객체 안 name 이 400, message 가 specified unit is not valid. |
| 업비트 | 없는 마켓 | 404 | error 객체 안 name 이 404, message 가 Code not found |
| 빗썸 2.0 | 없는 캔들 단위 | 200 | error 객체 안 name 이 400, message 가 Type mismatch error. Check the parameters type! |
| 빗썸 2.0 | 없는 마켓 | 200 | error 객체 안 name 이 404, message 가 Code not found |
| 빗썸 1.0 | 없는 페어 | 200 | status 가 5500, message 가 상장 코인이 아닙니다 |
| 코인원 | 없는 차트 간격 | 200 | result 가 error, error_code 가 107, error_msg 가 Parameter value is wrong |
| 코인원 | 없는 마켓 | 200 | result 가 error, error_code 가 108, error_msg 가 Unknown CryptoCurrency |
| 코빗 | 없는 캔들 간격 | 400 | success 가 거짓, message 가 BAD_REQUEST |
| 코빗 | 없는 심볼 | 400 | success 가 거짓, message 가 INVALID_CURRENCY_PAIR |
정리하면 이렇습니다.
| 구분 | 표면 |
|---|---|
| 실제 오류 상태 코드를 주는 곳 | 2곳 — 업비트 · 코빗 |
| 잘못된 요청에도 200 을 주는 곳 | 3곳 — 빗썸 2.0 · 빗썸 1.0 · 코인원 |
| 합 | 5개 표면 |
특히 눈에 띄는 자리는 빗썸 2.0 입니다. 본문 모양이 업비트와 똑같은 error 객체이고 그 안의 name 에 400 또는 404 가 들어 있습니다. 즉 진짜 상태 코드는 본문 안에 적혀 있고 전선에는 200 이 실려 옵니다. 없는 마켓에 대한 메시지 문자열 Code not found 는 업비트가 준 문자열과 글자까지 같았습니다.
이게 왜 성가시냐면, 대부분의 HTTP 클라이언트 라이브러리가 상태 코드로 성공과 실패를 가르기 때문입니다. 200이 오면 예외를 던지지 않고 그대로 다음 줄로 넘어갑니다. 그다음 줄이 배열을 기대하고 있었다면 거기서 엉뚱한 오류가 나거나, 더 나쁘게는 빈 값이 조용히 흘러갑니다.
응답 코드만 보고 성공 여부를 판단하는 코드는 이 세 곳에서 오류를 정상으로 읽습니다. 국내 거래소를 여러 곳 붙이는 프로그램이라면 상태 코드 검사 다음에 본문 안의 오류 표지를 거래소별로 한 번 더 확인하는 층이 필요합니다. 그 표지의 키 이름도 error · status · result 로 제각각이라 공통 함수 하나로는 처리되지 않습니다.
업비트 — 문서에 적힌 그룹 이름 다섯 중 둘이 실제 응답과 다릅니다
업비트는 요청을 그룹으로 나눠 세고, 그 그룹 이름을 Remaining-Req 헤더의 group 값에 실어 보냅니다. 문서에도 그룹 표가 있습니다. 그런데 문서에 적힌 이름과 응답이 돌려준 이름이 전부 같지는 않았습니다.
| 문서가 적은 그룹 이름 | 실제 헤더의 group 값 | 일치 |
|---|---|---|
market | market | 일치 |
ticker | ticker | 일치 |
orderbook | orderbook | 일치 |
candle | candles | 불일치 |
trade | crix-trades | 불일치 |
다섯 중 셋만 맞았습니다. 특히 체결 내역 쪽은 문서가 trade 라고 적었는데 응답은 crix-trades 를 돌려줬습니다. crix-trades 라는 문자열은 업비트 문서 어디에도 나오지 않습니다. 제가 지어낸 말이 아니라 창구가 실제로 돌려준 값입니다.
그룹 이름으로 잔여 수를 관리하는 코드를 문서만 보고 짜면, 이 두 자리에서 키가 어긋나 잔여 수가 엉뚱한 통에 쌓입니다.
관측 규모를 밝혀 두겠습니다. Remaining-Req 헤더를 받은 응답은 76건입니다. 그룹별로는 candles 45건, market 9건, orderbook 9건, ticker 9건, crix-trades 4건입니다.
min 값은 76건 내내 한 번도 움직이지 않았습니다
업비트 문서는 잔여 요청 수 확인 방법 절에서 REST API 응답의 Remaining-Req 헤더로 잔여 요청 수를 확인할 수 있다고 적고, 예시 값으로 group=default; min=1800; sec=29 를 실어 두었습니다. 그리고 필드 셋을 이렇게 설명합니다.
| 필드 | 업비트가 적은 설명 |
|---|---|
group | 요청이 속한 Rate Limit 그룹 |
min | Deprecated 표시가 붙어 있고, 분 단위 필드이며 더 이상 사용되지 않는 필드이니 응답 처리 시 참조하지 않는 것을 권장한다고 적혀 있습니다 |
sec | 현재 잔여 요청 수이고, 0이면 일정 시간 후에 다시 요청해야 한다고 적혀 있습니다 |
실측이 이 설명과 맞았습니다. 제가 받은 76건 전부에서 min 값은 600 이었고 단 한 번도 움직이지 않았습니다. 반면 sec 은 0부터 9까지 전부 나타났습니다.
그러니 min 을 보고 분당 몇 건 남았는지 계산하는 코드가 있다면 그 값은 아무것도 세지 않는 숫자입니다. 문서가 참조하지 말라고 적어 둔 필드이기도 합니다.

업비트 — 그룹은 진짜로 따로 돕니다
그룹이 이름만 다른 표시인지, 아니면 정말로 통이 따로인지를 확인했습니다. 여기서 HTTP 429 를 한 번 받아 봤습니다.
먼저 이 대목을 어떻게 다뤘는지 밝혀 두겠습니다. 이 측정은 그룹이 독립인지를 확인하려고 한 번만, 의도적으로 돌린 것입니다. 천장이 어디인지 알아보려고 창구를 반복해서 두드리는 일은 권하지 않습니다. 정상적인 클라이언트가 할 일은 한도를 시험하는 것이 아니라 헤더에 적힌 잔여 수를 읽고 그 앞에서 멈추는 것입니다. 아래 숫자는 그 잔여 수가 실제로 무엇을 세고 있는지 확인하기 위한 기록입니다.
첫째 장면. 캔들 그룹으로 연속 호출했더니 sec 이 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 으로 한 칸씩 내려갔고 11번째 요청에서 HTTP 429 가 왔습니다. 10건이 200, 1건이 429 였고, 429 가 온 시점은 첫 요청에서 0.389초 뒤입니다.
둘째 장면. 그 429 직후에 곧바로 시세 창구를 불렀습니다. 돌아온 값은 group=ticker; min=600; sec=9 였습니다. 한쪽이 막혀 있는데 다른 그룹은 가득 차 있었습니다.
셋째 장면. 마켓 그룹을 다섯 번 불러 sec 을 5까지 내린 직후 호가 창구를 부르니 sec 이 9 였습니다.
반대 방향도 확인했습니다. 같은 그룹 안에서는 경로가 달라도 한 통을 씁니다. 캔들 그룹에서 일 캔들을 세 번 불러 sec 이 9, 8, 7 로 내려간 뒤 초 캔들을 부르니 6에서 이어졌고 다음이 5였습니다. 경로가 바뀌었는데 숫자가 끊기지 않았습니다.
업비트 창구를 종류별로 전수로 훑어 본 사례는 이 사이트가 업비트 호가 단위 체계를 17,160행으로 세어 본 글에 따로 적어 두었습니다. 그때도 창구 하나를 오래 부르는 작업이었는데, 그룹이 따로 돈다는 사실을 알고 있으면 어느 창구를 어느 속도로 부를지 나누기가 쉬워집니다.
429 본문은 다른 오류와 모양이 다릅니다
한 가지 더 있습니다. 업비트의 오류 본문은 상황에 따라 구조 자체가 다릅니다.
| 상황 | 본문 구조 |
|---|---|
| 파라미터가 틀렸을 때 | error 객체 안에 name 과 message |
| 없는 마켓을 물었을 때 | error 객체 안에 name 과 message |
| 한도를 넘겼을 때 | error 객체 없이 최상위에 name 하나, 값은 too_many_requests |
앞의 둘은 한 겹 감싸여 있고 429만 평평합니다. 오류 본문을 파싱하는 코드가 error 키를 무조건 기대하고 있으면 429에서만 파싱이 깨집니다. 하필 제일 자주 마주치게 될 오류입니다.
참고로 429 응답에도 Remaining-Req 헤더는 실려 있었고 값은 group=candles; min=600; sec=0 이었습니다.
업비트 문서는 상태 코드 표에 두 줄을 더 적어 두었습니다. 429 Too Many Requests 는 초당 한도 초과이며 다음 초 경계까지 대기 후 재시도하라고 되어 있고, 418 I am a teapot 은 429 누적으로 일시 차단된 상태이며 응답에 포함된 차단 시간 정보를 확인한 뒤 안내된 시간 이후 재시도하라고 되어 있습니다.
저는 418 을 받아 본 적이 없습니다. 일부러 유발해 보지도 않았습니다. 그러니 그 응답이 실제로 어떤 모양인지, 차단 시간 정보가 어디에 실리는지는 이 글이 답하지 못합니다.
빗썸 — 두 세대가 각자 통을 가지고 있습니다
빗썸은 구세대 창구와 신세대 창구를 둘 다 열어 두고 있습니다. 그러면 자연스러운 물음이 하나 생깁니다. 둘이 한 통을 나눠 쓰는가, 아니면 각자 세는가.
세 장면으로 확인했습니다.
| 순서 | 시점 | 부른 곳 | X-RateLimit-Remaining |
|---|---|---|---|
| 1 | 0초에서 0.894초 | 신세대 창구 30회 연속 | 149에서 120으로 (29칸 감소) |
| 2 | 0.909초 | 구세대 창구 1회 | 149 |
| 3 | 0.932초 | 신세대 창구 1회 | 119 |
30번을 부르는 데 0.894초가 걸렸으니 초당 33.6회입니다. 그리고 그 직후 구세대 창구를 부르니 거의 가득 찬 149 가 돌아왔습니다. 다시 신세대를 부르니 119, 즉 자기 계단을 그대로 이어갔습니다.
한 통을 나눠 썼다면 3번 자리에서 148 근처가 나왔어야 합니다. 그렇지 않았습니다. 두 세대가 각각 자기 통을 가지고 있다고 읽는 편이 제가 받은 값과 맞습니다.
구세대 창구를 아직 쓰고 계신다면 이 대목이 실무적으로 의미가 있습니다. 신세대로 옮기는 중에 두 세대를 같이 부르고 있어도, 적어도 이 관측에서는 한쪽이 다른 쪽의 몫을 깎아먹지 않았습니다.
버스트 한도와 보충률 값은 제가 받은 모든 응답에서 각각 150 하나뿐이었습니다.
한 가지 더 적어 둡니다. 보충률 헤더에 150 이 실려 있지만, 0.894초 동안 30번을 부르는 사이에 잔여 값은 요청 한 번에 한 칸씩만 줄었고 되돌아오는 움직임은 보이지 않았습니다. 이 문장은 제가 받은 값에 대한 서술일 뿐이고 내부 동작을 단정하는 말이 아닙니다. 앞에서 적었듯 빗썸 문서에서 이 필드들의 뜻을 확인하지 못했기 때문에, 저 숫자가 초당 보충인지 다른 단위인지조차 이 글은 말할 수 없습니다.
빗썸 응답에만 실려 있는 필드를 세어 본 사례는 어제 올린 빗썸의 경보 종료 일시를 40행으로 세어 본 글에 있습니다. 그 글은 응답의 내용물이 거래소마다 다른 자리를 다뤘고, 이 글은 응답의 봉투가 다른 자리를 다룹니다.
코인원 — 숫자는 가장 큰데 확인할 문장을 찾지 못했습니다
코인원의 값은 넷 중 가장 넉넉해 보입니다.
| 헤더 | 관측된 값 |
|---|---|
Public-Ratelimit-Burst-Capacity | 1200 하나뿐 |
Public-Ratelimit-Replenish-Rate | 20 하나뿐 |
연속 10회 호출에서 잔여 값은 1199, 1198, 1197, 1196, 1195, 1194, 1193, 1192, 1191, 1190 으로 한 칸씩 내려갔습니다. 걸린 시간은 0.343초입니다. 요청 한 번에 정확히 한 칸씩 줄었습니다.
그런데 여기서 멈춰야 합니다. 이 숫자들이 무엇을 뜻하는지 코인원이 스스로 적어 둔 문장을 찾지 못했습니다. 문서 페이지는 200으로 열리지만 원문 HTML 안에 헤더 이름이 없습니다. 1200이 어떤 시간 창의 한도인지, 20이 초당인지 분당인지, 이 글은 알지 못합니다.
그래서 이 절은 값이 어떻게 움직였는지만 적습니다. 1200이라는 수를 보고 초당 1200번을 부를 수 있다고 읽으시면 안 됩니다. 그런 문장은 어디에도 확인되지 않았습니다.
코빗 — 한도는 실재하는데 천천히 부르면 헤더가 알려 주지 않았습니다
코빗은 문서 문언을 확인할 수 있었던 두 곳 중 하나입니다. 문서는 이렇게 적었습니다. 잔여 요청 수는 HTTP 응답에서 Ratelimit 헤더의 remaining 값을 보면 되고, limit 는 해당 API Endpoint 가 허용하는 최대 요청 수, remaining 은 남은 허용 요청 수, reset 은 요청 수 제한 초기화까지 남은 시간이며 단위는 초입니다. Ratelimit-Policy 헤더는 최대 요청 수와 시간 윈도우를 표시한다고 적혀 있습니다. 정책은 공개 API 초당 50회이고 기준은 IP 주소라고 되어 있습니다.
그런데 실제로 재 보니 자리가 둘로 갈렸습니다.
순차로 부를 때 — 40건 내내 숫자가 그대로였습니다
| 항목 | 값 |
|---|---|
| 보낸 요청 | 40건 |
| 걸린 시간 | 1.630초 (초당 24.5회) |
| HTTP 200 | 40건 |
| HTTP 429 | 0건 |
Ratelimit 값 | 40건 전부 limit=50, remaining=49, reset=1 |
문서대로라면 1초짜리 창 안에서 스무 번 넘게 불렀으니 remaining 이 내려가야 합니다. 내려가지 않았습니다. 서로 다른 값이 0종류, 같은 값이 40번입니다.
왜 그렇게 보였는지는 모릅니다. 이 글은 관측만 적고 메커니즘을 주장하지 않습니다. 바깥에서 헤더만 보고 있는 처지라 내부에서 무엇이 어떻게 세어지는지 알 방법이 없습니다.
동시에 부를 때 — 한도는 확실히 실재했습니다
같은 창구에 32개 작업자로 120건을 0.140초 안에 보냈습니다. 초당 857회입니다. 이 역시 한도가 실재하는지 한 번 확인하려고 의도적으로 돌린 측정이고, 일상적으로 할 일이 아닙니다.
| 항목 | 값 |
|---|---|
| 보낸 요청 | 120건 |
| HTTP 200 | 57건 |
| HTTP 429 | 63건 |
remaining 에 나타난 값의 종류 | 0부터 49까지 50가지 전부 |
remaining 이 0인데 200이었던 응답 | 5건 |
remaining 이 0보다 큰데 429였던 응답 | 0건 |
1부터 49까지 중 46개 값은 정확히 한 번씩 나타났고, 세 개 값(2와 15와 33)은 두 번씩 나타났습니다. 동시에 날아간 요청들이 같은 계수기를 함께 건드린 모습입니다.
429 본문은 63건 전부 같은 모양이었습니다. success 가 거짓이고 code 가 429, message 가 TOO_MANY_REQUESTS 입니다.
정리하면 한도는 실재하고 작동합니다. 다만 초당 24.5회로 고르게 부르는 동안에는 헤더의 숫자가 소진을 반영하지 않았습니다. 헤더의 잔여 수만 보고 아직 여유가 있다고 판단하는 코드는 이 자리에서 근거를 잃습니다.

코빗 — 재시도 대기 값이 한 종류가 아니었습니다
429 를 받은 63건에는 Retry-After 헤더가 실려 있었습니다. 그런데 값이 하나가 아니었습니다.
Retry-After 값 | 건수 |
|---|---|
| 1 | 32 |
| 0 | 31 |
절반가량이 0 입니다. 코빗 문서의 예시에는 이 값이 1로 실려 있습니다.
이게 실무에서 무슨 뜻인지 짚어 두겠습니다. Retry-After 만큼 기다렸다가 다시 보내는 코드는 이 31건에서 0초를 기다립니다. 즉 429를 받자마자 곧바로 다시 보냅니다. 그러면 또 429가 오고, 또 0초를 기다리고, 그 자리에서 요청이 계속 튀어나가는 고리가 만들어집니다.
그래서 Retry-After 를 그대로 신뢰하는 대신 최소 대기 시간을 코드 쪽에서 정해 두는 편이 안전합니다. 서버가 알려 준 값과 자기가 정한 하한 중 큰 쪽을 쓰는 방식입니다. 애초에 429까지 가지 않도록 잔여 수를 보고 미리 멈추는 것이 먼저이고, Retry-After 는 그래도 걸렸을 때의 마지막 장치입니다.
코빗 — 오류 응답에서 헤더가 사라지는 자리가 있습니다
마지막 관측입니다. 네 가지 상황을 각각 다섯 번씩, 모두 20건을 확인했습니다.
| 상황 | HTTP | 시도 | Ratelimit 헤더가 실린 응답 |
|---|---|---|---|
| 정상 조회 | 200 | 5 | 5 |
| 없는 심볼 | 400 | 5 | 5 |
| 없는 캔들 간격 | 400 | 5 | 0 |
| 없는 경로 | 404 | 5 | 0 |
20건 중 10건에 헤더가 있었고 10건에는 없었습니다. 상황별로는 다섯 번 시도가 전부 같은 결과였으니, 무작위가 아니라 상황에 따라 갈립니다.
같은 400인데도 갈린다는 점이 중요합니다. 없는 심볼은 헤더가 실려 왔고 없는 캔들 간격은 실려 오지 않았습니다. 겉으로는 같은 오류처럼 보이는 두 응답이 서로 다르게 생겼습니다.
잔여량을 헤더로만 추적하는 클라이언트는 이 두 상황에서 자기 몫을 볼 수 없습니다. 헤더가 없으면 마지막으로 본 값을 그대로 들고 있게 되는데, 그사이 실제 잔여는 줄어 있을 수 있습니다. 헤더가 없을 때 어떻게 할지를 코드에 적어 두지 않으면 이 자리가 조용히 비어 있게 됩니다.
여기서 범위를 하나 좁혀 두겠습니다. 404 를 받은 두 경로는 제가 이름을 지어 넣어 본 것입니다. 코빗이 그런 창구를 두지 않았다는 증거가 아니라, 제가 부른 이름이 없다는 뜻일 뿐입니다. 그래서 그 두 건은 코빗의 API 표면에 대해 아무것도 증명하지 않습니다. 다만 없는 경로를 불렀을 때 헤더가 실리지 않더라는 관측 자체는 그대로 남습니다.
두 시점에서 다시 재 봤습니다
한 번 받은 값으로 관행을 말하면 위험합니다. 그래서 같은 창구를 같은 방식으로 두 번 불렀습니다. 1차는 오전 10시 41분부터 10시 45분, 2차는 10시 50분 40초입니다.
| 항목 | 1차 | 2차 | 결과 |
|---|---|---|---|
| 업비트 헤더 이름 | Remaining-Req | Remaining-Req | 같음 |
| 빗썸 헤더 이름 네 종 | X-RateLimit 계열 | X-RateLimit 계열 | 같음 |
| 코인원 헤더 이름 세 종 | Public-Ratelimit 계열 | Public-Ratelimit 계열 | 같음 |
| 코빗 헤더 이름 두 종 | Ratelimit · Ratelimit-Policy | 같음 | 같음 |
| 잘못된 요청 9건의 HTTP 상태 | 위 표 그대로 | 위 표 그대로 | 9건 중 9건 동일 |
헤더 이름과 상태 코드 관행은 두 시점에서 하나도 달라지지 않았습니다. 잔여량 숫자는 요청을 보낼 때마다 바뀌는 값이라 애초에 대조 대상이 아닙니다.
그렇다고 이 값들이 앞으로도 같으리라는 뜻은 아닙니다. 여기 적힌 한도와 헤더 이름과 상태 코드 관행은 거래소가 예고 없이 바꿀 수 있습니다. 이 글은 2026년 9월 9일 오전 11분 남짓한 창 안의 응답만 말합니다. 두 시점이 같았다는 것은 그날 그 시간대에 값이 흔들리지 않았다는 확인일 뿐, 다음 달에도 같다는 약속이 아닙니다.
그래서 클라이언트를 어떻게 짜야 하는가
관측을 실무 문장으로 줄이면 다섯 줄입니다.
첫째, 상태 코드만으로 성공을 판정하지 마십시오. 다섯 표면 중 셋이 잘못된 요청에 200을 돌려줬습니다. 상태 코드를 본 뒤 본문의 오류 표지를 거래소별로 한 번 더 봐야 합니다. 그 표지의 키 이름이 error · status · result 로 갈리므로 거래소마다 따로 써야 합니다.
둘째, 헤더 이름을 상수 하나로 두지 마십시오. 네 곳이 함께 쓰는 이름이 0개이고, 값의 모양도 한 헤더에 여러 쌍을 넣는 쪽과 헤더를 쪼개는 쪽으로 갈립니다.
셋째, 문서에 적힌 이름을 그대로 키로 쓰기 전에 응답을 한 번 찍어 보십시오. 업비트에서 다섯 중 둘이 어긋났고, 그중 하나는 문서에 아예 없는 문자열이었습니다.
넷째, 잔여 수가 있는 쪽에서는 그 수를 보고 미리 멈추십시오. 한도가 어디인지 두드려 확인할 이유가 없습니다. 다만 헤더가 항상 정확하다고 가정하지도 마십시오. 코빗에서는 40건 내내 값이 움직이지 않았고, 오류 응답 절반에는 헤더가 아예 없었습니다.
다섯째, 재시도 대기 값에 하한을 두십시오. 서버가 준 값이 0일 수 있습니다. 63건 중 31건이 그랬습니다.
한 가지 덧붙이면, 거래소 앱마다 새로고침 감각이 다르게 느껴지는 이유의 일부가 이 자리에 있습니다. 어떤 곳은 그룹별로 통을 나눠 두어 한쪽이 막혀도 다른 화면이 살아 있고, 어떤 곳은 세대별로 통이 갈려 있습니다. 겉으로 보이는 화면은 비슷해도 뒤에서 세는 방식이 넷 다 다릅니다.
확인하지 못한 것
정직하게 적어 두겠습니다.
- 빗썸과 코인원의 문서 문언을 확인하지 못했습니다. 두 문서 루트는 HTTP 200을 주지만 받아 온 HTML 안에 헤더 이름이 0회입니다. 스크립트로 화면을 그리는 형태라 원문에 문자열이 없습니다. 그 두 절은 제가 읽은 응답에만 근거합니다. 업비트와 코빗의 문언은 200으로 열린 문서 페이지에서 인용한 것입니다.
- 코빗의 순차 호출에서
remaining이 49에 머문 이유를 모릅니다. 관측일 뿐이며 메커니즘을 주장하지 않습니다. - 업비트가 문서에 적어 둔 418 을 받아 본 적이 없습니다. 유발을 시도하지도 않았습니다. 그 응답의 모양과 차단 시간 정보의 위치는 이 글이 답하지 못합니다.
- 코빗에서 404 가 난 두 경로는 제가 지어낸 이름입니다. 그 거래소의 API 표면에 대해 아무것도 증명하지 않습니다. 업비트 쪽에서 400 이 난 경로 하나도 마찬가지로 제가 추측해 넣은 것입니다.
- 인증이 필요한 엔드포인트는 하나도 부르지 않았습니다. 주문이나 잔고 쪽 한도는 이 글의 범위 밖이고, 계정을 붙이면 헤더도 한도도 달라질 수 있습니다.
- 고팍스는 포함하지 않았습니다. 국내 원화 거래소 넷만 봤습니다.
- 한도의 절대값을 비교하지 않았습니다. 시간 창의 정의가 확인되지 않은 곳이 있어 1200과 150과 50을 같은 자로 놓고 견줄 수 없습니다.
- 매매 판단을 담고 있지 않습니다. 이 글에는 시세도 종목도 나오지 않습니다. 디지털자산은 원금 손실 위험이 있고 모든 판단은 본인 책임입니다.
정리
세 줄로 줄이면 이렇습니다.
첫째, 잘못된 요청에 진짜 오류 상태 코드를 주는 표면은 다섯 중 둘이었습니다. 업비트와 코빗뿐이고, 빗썸 두 세대와 코인원은 200을 돌려줬습니다. 특히 빗썸 2.0 은 진짜 상태 코드를 본문 안에 적어 보냅니다.
둘째, 네 거래소가 함께 쓰는 잔여 요청 헤더 이름은 0개였습니다. 이름도 값의 모양도 넷이 전부 다르고, 업비트에서는 문서가 적은 그룹 이름 다섯 중 둘이 실제 응답과 어긋났습니다.
셋째, 헤더의 숫자를 그대로 믿기 어려운 자리가 있었습니다. 업비트의 min 은 76건 내내 600에 붙박여 있었고 문서 자신이 Deprecated 로 표시해 두었으며, 코빗은 40건 내내 잔여가 49였고 오류 응답 스무 건 중 열 건에서는 헤더가 아예 사라졌으며, 재시도 대기 값은 63건 중 31건이 0이었습니다.
여기 적힌 값은 2026년 9월 9일 오전 한 구간의 응답을 세어 둔 기록일 뿐입니다. 거래소는 한도도 헤더 이름도 상태 코드 관행도 예고 없이 바꿀 수 있고, 지금 다시 부르면 다른 값이 나올 수 있습니다. 이 글은 어떤 종목의 매매도 권하지 않습니다.