AlgoLab Blog · 바이비트(Bybit) V5 API · 트러블슈팅 · 2026

바이비트 API 10002 — recv_window로 안 풀릴 때

코인 · 바이비트 API 2026-11-01 · 약 7분 읽기 · 알고랩 AlgoLab
한 줄 요약

바이비트(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 실측 기준)

목차

  1. 10002가 나는 정확한 조건 — 창은 비대칭이다
  2. 실측: 우리 PC는 바이비트보다 4.14초 늦었다
  3. 10002와 헷갈리는 인증 에러 6개
  4. pybit는 10002를 어떻게 처리하나 (소스 확인)
  5. 고치는 코드 — 서버 시각 오차 보정
  6. 점검 순서
  7. 자주 묻는 질문

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입니다.

X-BAPI-TIMESTAMP 허용 창 (기본 recv_window = 5000ms) server_time −5000ms +1000ms PC가 늦을 때 → 여기 찍힘 (recv_window로 늘릴 수 있음) ◀ 왼쪽 끝: recv_window를 키우면 넓어진다 오른쪽 끝: 고정 +1000 ▶ 출처: Bybit V5 공식 문서 Integration Guidance의 부등식을 그림으로 옮김 (축척 대략)
PC 시계가 1초 넘게 빠르면 recv_window를 몇으로 올려도 오른쪽 끝을 넘는다

공식 에러 목록의 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공식 설명(원문 요약)원인봇의 대응
10002The request time exceeds the time window range.PC 시계 오차·느린 네트워크시계 보정 후 재시도 가능
10003API key is invalid. Check whether the key and domain are matched (mainnet, testnet, mainnet-demo, testnet-demo)키 오타·도메인 불일치즉시 중단 — 재시도 무의미
10004Error sign, please check your signature generation algorithm.서명 문자열 순서·JSON 직렬화 차이즉시 중단 — 코드 수정
10005Permission denied, please check your API key permissions.키에 거래 권한 없음즉시 중단 — 키 설정 변경
10010Unmatched IP, please check your API key's bound IP addresses.IP 화이트리스트 밖(서버 이전·공유기 재부팅)즉시 중단 — 알림
10006 / 10018Too 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는 인증 단계에서 거절된 것이라 일반적으로 주문이 접수되지 않습니다. 그래도 확실히 하려면 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. 점검 순서

바이비트 봇 전체 구조(계정 종류, 선물·현물, 데모 트레이딩)는 바이비트 자동매매 입문 가이드에서, 호출 한도(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시간 빠른 답변 가능합니다.

무료 상담 시작하기