바이비트 API 10002 — recv_window로 안 풀릴 때
바이비트(Bybit) V5 API의 retCode 10002는 "요청 시각이 허용 창을 벗어났다"는 뜻이고, 공식 조건은 server_time − recv_window ≤ timestamp < server_time + 1000입니다. 창이 비대칭이라 PC 시계가 늦을 때는 recv_window(기본 5000ms)를 늘리면 통과하지만, PC 시계가 서버보다 1초 이상 빠르면 recv_window를 아무리 올려도 계속 실패합니다. pybit 5.17.0은 10002를 받으면 recv_window를 2,500ms씩 늘리며 3초 쉬고 최대 3번 재시도하는데, 이것도 "늦은 시계"만 구합니다. 근본 해결은 PC 시계를 NTP로 맞추거나 /v5/market/time으로 오차를 재서 보정하는 것입니다. 저희 PC를 실제로 재 보니 4.14초 늦어 기본 창 5,000ms의 여유가 0.86초뿐이었습니다. (Bybit 공식 문서·pybit 5.17.0 소스·2026-11-01 실측 기준)
목차
- 10002가 나는 정확한 조건 — 창은 비대칭이다
- 실측: 우리 PC는 바이비트보다 4.14초 늦었다
- 10002와 헷갈리는 인증 에러 6개
- pybit는 10002를 어떻게 처리하나 (소스 확인)
- 고치는 코드 — 서버 시각 오차 보정
- 점검 순서
- 자주 묻는 질문
1. 10002가 나는 정확한 조건 — 창은 비대칭이다
바이비트 V5에서 잔고 조회(/v5/account/wallet-balance)나 주문(/v5/order/create) 같은 개인 API는 헤더 4개로 인증합니다. X-BAPI-API-KEY, X-BAPI-TIMESTAMP(밀리초 UTC), X-BAPI-SIGN(HMAC_SHA256 서명), 그리고 선택 헤더 X-BAPI-RECV-WINDOW입니다. 공식 문서는 X-BAPI-RECV-WINDOW를 "요청이 유효한 시간(밀리초), 기본값 5,000"이라고 설명하고, 타임스탬프가 지켜야 할 규칙을 이렇게 적어 둡니다.
# Bybit V5 공식 문서 (Integration Guidance) 원문
server_time - recv_window <= timestamp < server_time + 1000
# = 내 timestamp는 [server_time - recv_window, server_time + 1000) 안에 있어야 한다
읽는 법이 중요합니다. 아래쪽 한계는 recv_window로 늘릴 수 있지만, 위쪽 한계는 +1,000ms로 고정입니다. 즉 내 PC 시계가 늦어서 타임스탬프가 과거로 찍히면 창을 넓혀 구제할 수 있고, PC 시계가 빨라서 미래로 찍히면 1초만 넘어도 무조건 10002입니다.
공식 에러 목록의 10002 설명은 "The request time exceeds the time window range." 한 줄입니다. 실제 응답 메시지는 커뮤니티 보고상 req_timestamp[...]·server_timestamp[...]·recv_window[5000]를 함께 담아 오는 형식이 많으므로, 로그에 retMsg 전체를 남겨 두면 두 시각의 차이로 방향(빠름/늦음)을 바로 읽을 수 있습니다. 메시지 문구는 바뀔 수 있으니 코드는 retCode 숫자로만 분기하세요.
2. 실측: 우리 PC는 바이비트보다 4.14초 늦었다
키가 필요 없는 공개 API GET /v5/market/time을 메인넷·테스트넷·데모 세 도메인에 보내고, 요청 직전·직후 로컬 시각의 중간값과 서버 시각을 비교했습니다(2026-11-01 08:05 KST, Windows 11, 서울).
GET https://api.bybit.com/v5/market/time
{"retCode":0,"retMsg":"OK","result":{"timeSecond":"1793487948","timeNano":"1793487948369898687"},"retExtInfo":{},"time":1793487948369}
https://api.bybit.com 로컬 − 서버 = -4187 ms (왕복 167 ms)
https://api-testnet.bybit.com 로컬 − 서버 = -4215 ms (왕복 187 ms)
https://api-demo.bybit.com 로컬 − 서버 = -4221 ms (왕복 234 ms)
세 도메인 모두 약 −4.2초입니다. 바이비트가 아니라 우리 PC 쪽 문제인지 확인하려고 Windows 시간 서비스로 구글 NTP와도 비교했습니다.
PS> w32tm /stripchart /computer:time.google.com /samples:3 /dataonly
08:05:58, +04.1435794s
08:06:00, +04.1439847s
08:06:02, +04.1446515s
PS> w32tm /query /status
윤초 조정: 3(동기화되지 않음)
계층: 0(지정되지 않음)
마지막으로 동기화한 시간: 지정되지 않음
원본: Local CMOS Clock
+04.14s는 "NTP 서버가 내 PC보다 4.14초 앞서 있다"는 뜻이라 바이비트 측정값과 일치합니다. 원본이 Local CMOS Clock, 동기화 기록이 "지정되지 않음" — 이 PC는 한 번도 인터넷 시간과 맞춘 적이 없었습니다. 2초 간격으로 세 번 재는 4초 사이에도 오차가 0.0011초 커졌습니다.
지금 이 상태로 봇을 돌리면 어떻게 될까요. 타임스탬프가 서버보다 약 4,190ms 과거로 찍히므로 기본 창(−5,000ms) 안에는 들어갑니다. 여유는 0.86초뿐입니다. 네트워크가 잠깐 느려지거나 시계가 조금 더 밀리면 그날부터 10002가 섞여 나옵니다. "어제까지 멀쩡했는데 갑자기"라는 증상이 대부분 이런 경계 상태에서 나옵니다. 시계 오차가 시간이 지나며 커지는 모습은 봇 PC 시계 동기화 실측 글에 따로 정리했습니다.
WebSocket 개인 채널은 더 빡빡합니다. 공식 문서의 인증 예시는 expires = int((time.time() + 1) * 1000)이고, expires는 "현재 타임스탬프보다 커야" 합니다. pybit의 private_auth_expire 기본값도 1(초)입니다. PC가 4.14초 늦으면 로컬 기준 +1초는 서버 기준 이미 3초 전이라, 주문·포지션 실시간 채널(order·position·execution) 인증 조건을 처음부터 못 맞춥니다. REST는 되는데 WebSocket 인증만 안 되면 시계부터 보세요.
3. 10002와 헷갈리는 인증 에러 6개
인증 단계 에러는 번호가 붙어 있어서 헷갈리기 쉽습니다. Bybit V5 공식 에러 목록 원문을 옮기고, 봇이 어떻게 대응해야 하는지 붙였습니다.
| retCode | 공식 설명(원문 요약) | 원인 | 봇의 대응 |
|---|---|---|---|
| 10002 | The request time exceeds the time window range. | PC 시계 오차·느린 네트워크 | 시계 보정 후 재시도 가능 |
| 10003 | API key is invalid. Check whether the key and domain are matched (mainnet, testnet, mainnet-demo, testnet-demo) | 키 오타·도메인 불일치 | 즉시 중단 — 재시도 무의미 |
| 10004 | Error sign, please check your signature generation algorithm. | 서명 문자열 순서·JSON 직렬화 차이 | 즉시 중단 — 코드 수정 |
| 10005 | Permission denied, please check your API key permissions. | 키에 거래 권한 없음 | 즉시 중단 — 키 설정 변경 |
| 10010 | Unmatched IP, please check your API key's bound IP addresses. | IP 화이트리스트 밖(서버 이전·공유기 재부팅) | 즉시 중단 — 알림 |
| 10006 / 10018 | Too many visits / Exceeded the IP Rate Limit | 호출 한도 초과 | 대기 후 재시도 |
10003의 "4개 환경"이 특히 자주 걸립니다. 바이비트는 메인넷(api.bybit.com), 테스트넷(api-testnet.bybit.com), 메인넷 데모(api-demo.bybit.com), 테스트넷 데모 키가 서로 호환되지 않습니다. 데모 트레이딩에서 만든 키로 메인넷 주소를 부르면 키 자체는 멀쩡해도 무효 판정입니다.
그리고 HTTP 상태 코드가 200이 아닐 수도 있다는 점도 알아 두세요. 테스트넷에 존재하지 않는 키로 잔고 조회를 보내 봤더니 JSON 본문 없이 이렇게 왔습니다.
GET https://api-testnet.bybit.com/v5/account/wallet-balance?accountType=UNIFIED
(X-BAPI-API-KEY: 가짜 키, X-BAPI-TIMESTAMP: 현재 시각)
HTTP/2 401
content-type: application/json; charset=utf-8
content-length: 0
timenow: 1793487958096
server: Openresty
x-amz-cf-pop: ICN80-P4
retCode를 파싱하려던 코드는 여기서 JSONDecodeError로 죽습니다. 반면 응답 헤더 timenow에는 서버 시각이 들어 있어 오차 측정에 쓸 수 있습니다. 즉 봇은 ① HTTP 상태 코드 ② retCode 두 단계로 나눠 판정해야 합니다. 업비트 쪽 같은 분류는 업비트 API 에러코드 — 재시도할 것과 멈출 것에 있습니다.
4. pybit는 10002를 어떻게 처리하나 (소스 확인)
파이썬에서 바이비트 공식 SDK pybit를 쓰면 10002를 직접 처리하지 않아도 됩니다. 다만 무엇을 해 주고 무엇을 못 하는지는 알아야 합니다. PyPI 최신 pybit 5.17.0의 _http_manager.py 원문입니다.
# pybit 5.17.0 — pybit/_http_manager.py (발췌)
recv_window: bool = field(default=5000)
max_retries: bool = field(default=3)
retry_delay: bool = field(default=3)
...
if not self.retry_codes:
self.retry_codes = {10002, 10006, 30034, 30035, 130035, 130150}
...
def _handle_retryable_error(self, response, error_code, error_msg, recv_window):
delay_time = self.retry_delay
if error_code == 10002: # recv_window error
error_msg += ". Added 2.5 seconds to recv_window"
recv_window += 2500
...
self.logger.error(f"{error_msg}. Retrying...")
time.sleep(delay_time)
return recv_window
정리하면 이렇습니다.
- 재시도: 10002가 오면
recv_window를 5,000 → 7,500 → 10,000으로 늘리며 3초씩 쉬고 최대 3번(max_retries=3) 보냅니다. 타임스탬프는 매 시도마다_helpers.generate_timestamp()로 새로 찍습니다. - PC가 늦을 때: 오차가 5~10초 사이면 두 번째·세 번째 시도에서 통과합니다. 대신 주문 1건이 3~6초 늦게 나갑니다. 로그에는
Added 2.5 seconds to recv_window. Retrying...가 찍힙니다. - PC가 1초 넘게 빠를 때: 창 오른쪽 끝이 고정이라 세 번 모두 실패하고, 9초를 쉰 뒤
FailedRequestError("Bad Request. Retries exceeded maximum.")가 납니다. - 재시도는 매번 새로 시작: 늘린
recv_window는 그 호출 안에서만 유지됩니다. 다음 주문은 다시 5,000부터라, 시계가 6초 늦은 PC는 모든 주문에서 3초씩 손해를 봅니다. - HTTP 401·403:
_check_status_code가 상태 코드 200이 아니면 재시도 없이FailedRequestError("HTTP status code is not 200.")를 냅니다. 10003류는InvalidRequestError가 아니라 이쪽으로 올 수 있습니다.
재시도가 주문을 두 번 내지 않을까 걱정되면, 10002는 인증 단계에서 거절된 것이라 일반적으로 주문이 접수되지 않습니다. 그래도 확실히 하려면 orderLinkId를 직접 지정해 보내고, 애매하면 /v5/order/realtime으로 그 ID를 조회한 뒤 판단하세요. 타임아웃에서 같은 문제를 다룬 글이 ReadTimeout 난 주문 — 다시 보내면 두 번 산다입니다.
5. 고치는 코드 — 서버 시각 오차 보정
1순위는 PC 시계를 맞추는 것입니다. Windows라면 관리자 PowerShell에서 w32tm /resync, 자동 동기화가 꺼져 있으면 시간 서비스(W32Time) 설정부터 켜야 합니다. 시계를 못 만지는 환경이면(회사 PC, 권한 없음) 코드에서 오차를 재서 보정합니다.
import time, requests
import pybit._helpers as h
from pybit.unified_trading import HTTP
BASE = "https://api.bybit.com"
def measure_offset_ms(samples=5):
"""로컬 시계가 바이비트보다 몇 ms 늦은지(+)/빠른지(-) 잰다. 왕복이 가장 짧은 표본을 쓴다."""
best = None
for _ in range(samples):
t0 = time.time() * 1000
srv = int(requests.get(BASE + "/v5/market/time", timeout=5).json()["time"])
t1 = time.time() * 1000
rtt = t1 - t0
off = srv - (t0 + t1) / 2
if best is None or rtt < best[0]:
best = (rtt, off)
time.sleep(0.2)
return int(best[1])
OFFSET = measure_offset_ms() # 우리 PC: 약 +4170
print("offset ms:", OFFSET)
if abs(OFFSET) > 1000:
print("경고: PC 시계 오차 1초 초과 — w32tm /resync 권장")
# pybit 내부 타임스탬프 함수를 보정값으로 덮어쓴다 (REST·WebSocket 공통)
_orig = h.generate_timestamp
h.generate_timestamp = lambda: _orig() + OFFSET
session = HTTP(testnet=False, api_key=KEY, api_secret=SECRET, recv_window=5000)
요점은 세 가지입니다. ① /v5/market/time은 키 없이 부를 수 있어 시작할 때 한 번 재면 됩니다. ② 왕복 시간의 절반만큼 오차가 섞이므로 여러 번 재서 왕복이 가장 짧은 값을 씁니다. ③ 시계는 계속 밀리므로(위 실측에서 4초에 0.0011초) 봇이 며칠씩 돌면 1시간마다 다시 재는 편이 안전합니다. KEY·SECRET은 코드에 직접 쓰지 말고 환경 변수에서 읽으세요(API 키 보안 5가지).
pybit._helpers.generate_timestamp를 바꾸는 방식은 공식 기능이 아니라 내부 함수 교체라서, pybit 버전이 바뀌면 동작을 다시 확인해야 합니다. 직접 서명하는 코드라면 timestamp = str(int(time.time()*1000) + OFFSET) 한 줄이면 되고, 서명 문자열은 공식 규칙대로 timestamp + api_key + recv_window + queryString(POST는 JSON 본문)입니다. 이 순서가 틀리면 10002가 아니라 10004가 납니다.
6. 점검 순서
- ① 방향부터:
/v5/market/time오차가 음수(PC가 빠름)면 recv_window 조정은 소용없습니다. 바로 시계를 맞추세요. - ② 크기: PC가 늦고 오차가 recv_window의 절반을 넘으면 경계 상태입니다. 동기화 후 다시 재서 ±200ms 안쪽인지 봅니다.
- ③ 간헐적이면 네트워크: 오차는 작은데 가끔 10002가 나면 VPN·와이파이 지연입니다. 이때만 recv_window를 7,000~10,000 정도로 소폭 올립니다. 공식 문서도 창이 작을수록 안전하다고(재전송 공격 방지) 적고 있어 무작정 60,000으로 올리는 것은 권하지 않습니다.
- ④ WebSocket만 안 되면:
expires여유(기본 1초)를 의심합니다.pybit의WebSocket(..., private_auth_expire=...)로 늘릴 수 있지만, 이것도 시계 보정이 먼저입니다.
바이비트 봇 전체 구조(계정 종류, 선물·현물, 데모 트레이딩)는 바이비트 자동매매 입문 가이드에서, 호출 한도(10006) 설계는 바이낸스 API 제한 — weight와 IP 밴의 구조가 거의 같으니 함께 보시면 됩니다.
7. 자주 묻는 질문
바이비트 API 10002 에러는 무슨 뜻인가요?
요청 헤더 X-BAPI-TIMESTAMP가 허용 시간 창을 벗어났다는 뜻입니다. 공식 조건은 server_time - recv_window <= timestamp < server_time + 1000이고, recv_window 기본값은 5000ms입니다. 대부분 PC 시계 오차가 원인이며 API 키나 권한 문제가 아닙니다.
recv_window를 늘렸는데도 10002가 계속 납니다. 왜 그런가요?
PC 시계가 바이비트 서버보다 빠른 경우입니다. 허용 창의 과거 쪽은 recv_window로 넓어지지만 미래 쪽은 +1000ms로 고정이라, 시계가 1초 넘게 빠르면 recv_window를 얼마로 올려도 통과하지 못합니다. /v5/market/time으로 오차 방향을 확인하고 PC 시계를 NTP로 맞추세요.
pybit를 쓰면 10002를 자동으로 처리해 주나요?
pybit 5.17.0은 10002를 재시도 대상 코드로 두고, 받을 때마다 recv_window를 2500ms 늘리며 3초 쉬고 최대 3번 다시 보냅니다. PC 시계가 늦은 경우에는 통과하지만 주문이 3~6초 늦어지고, 시계가 빠른 경우에는 세 번 모두 실패한 뒤 FailedRequestError가 납니다.
10002와 10003, 10004는 어떻게 다른가요?
10002는 시각 문제, 10003은 API 키가 무효이거나 메인넷·테스트넷·데모 도메인이 키와 맞지 않는 경우, 10004는 서명 생성이 틀린 경우입니다. 10002만 시계 보정 후 재시도할 가치가 있고, 10003과 10004는 재시도해도 같은 결과라 즉시 멈추고 설정이나 코드를 고쳐야 합니다.
바이비트 봇, 에러에 멈추지 않게 만들어 드립니다
Bybit V5 API로 선물·현물 자동매매를 만들 때 시계 보정, 에러코드별 재시도·중단 분기, 텔레그램 알림까지 넣어 맞춤 제작합니다. 24시간 빠른 답변 가능합니다.
무료 상담 시작하기