응답을 캐시하고 API 429 오류 처리하기

429 rate_limit_exceeded는 Retry-After 헤더의 시간만큼 기다린 뒤 다시 시도하라는 뜻이고, 402 insufficient_credits는 잔액을 모두 썼으니 크레딧을 충전하라는 뜻입니다. 작은 캐시를 두면 반복 요청이 요금에 잡히지 않습니다. 만료 시간은 데이터가 바뀌는 빈도에 맞추세요.

API 사용 방법

  1. 키 준비하기 무료 API 키를 받아 NARWHAL_API_KEY 환경 변수에 보관하세요. Python 스크립트는 이 변수를 읽어 키를 Authorization: Bearer YOUR_KEY로 https://api.narwhalapi.com에 보냅니다.
  2. 데이터 종류별로 만료 시간 정하기 휴일 캘린더는 며칠에서 몇 주, 경제 지표 발표는 다음 예정 발표 때까지, 수익률 곡선은 하루 동안 캐시하세요. 일별 환율은 다음 UTC 날짜가 될 때까지 캐시해 둘 수 있습니다. 이 기준으로 유효 시간, 즉 TTL을 정하세요.
  3. 캐시 및 재시도 헬퍼 사용하기 캐시 키는 경로와 정렬된 파라미터를 합친 것이므로 파라미터가 다른 요청은 별도의 항목이 됩니다. 스크립트는 같은 경로와 TTL로 금 가격을 두 번 읽으며, 두 번째 읽기는 그 항목이 TTL 안에 남아 있는 동안 캐시된 응답을 사용합니다.
    Python: 캐시와 Retry-After 처리
    import os, random, time
    import requests
    
    API = "https://api.narwhalapi.com"
    session = requests.Session()
    session.headers["Authorization"] = f"Bearer {os.environ['NARWHAL_API_KEY']}"
    _cache: dict[str, tuple[float, dict]] = {}
    
    
    def get(path: str, ttl: int = 300, **params):
        """GET with a small in-memory cache, Retry-After on 429s and
        backoff on 5xx."""
        key = path + repr(sorted(params.items()))
        hit = _cache.get(key)
        if hit and time.time() - hit[0] < ttl:
            return hit[1]
        for attempt in range(4):
            r = session.get(API + path, params=params, timeout=10)
            if r.status_code == 402:
                raise RuntimeError("Out of credit; add credit in the dashboard.")
            if r.status_code == 429:
                time.sleep(
                    float(r.headers.get("Retry-After", 2))
                )  # short-window limit: wait, then retry
                continue
            if r.status_code in (500, 503):
                time.sleep(
                    2**attempt + random.random()
                )  # bounded exponential backoff with jitter
                continue
            r.raise_for_status()
            _cache[key] = (time.time(), r.json())
            return _cache[key][1]
        raise RuntimeError(f"Gave up on {path}")
    
    
    print(get("/v1/commodities/spot/XAU/USD", ttl=60)["price"])
    print(get("/v1/commodities/spot/XAU/USD", ttl=60)["price"], "(from cache)")
  4. 응답 헤더 읽기 성공한 데이터 응답에는 X-Request-ID, 분당 X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, 그리고 크레딧과 관련된 X-Narwhal-Cost와 X-Narwhal-Balance가 들어 있습니다.

무료 크레딧 계정은 분당 50건, 유료 크레딧 계정은 500건까지 요청을 보낼 수 있습니다.

헬퍼는 요청이 성공한 뒤에만 응답을 저장합니다. 만료되지 않은 항목은 HTTP 호출 전에 바로 반환되고, 만료된 항목은 다시 요청 루프로 들어갑니다. 캐시는 메모리에 있으므로, 웹 서비스에서 공유 캐시가 필요하다면 그 방식을 사용하세요.

기본 TTL은 300초이며 금 가격 호출은 ttl=60으로 덮어씁니다. 각 요청의 타임아웃은 10초이고 루프는 최대 4번 시도합니다. 짧은 시간 창의 제한에서는 헬퍼가 Retry-After를 초 단위 대기 시간으로 읽고 다음 시도 전에 기다립니다. 이 헤더가 없으면 2초를 사용합니다. 시도 횟수를 모두 쓰면 헬퍼는 요청한 경로를 담은 오류를 발생시킵니다. 그 밖의 실패 상태는 캐시에 응답을 추가하지 않고 raise_for_status()로 넘어갑니다.

코드 없이 재시도 처리하기

Zapier, Make, n8n에서는 짧은 지연 단계를 추가하거나 플랫폼의 오류 시 재시도 기능을 켜세요. 이 플랫폼들은 이미 HTTP 상태를 보여 줍니다. 429라면 문제 본문의 code를 읽고, 지연 후 재시도할지 중단할지 정하세요.

사용하는 데이터에 알맞은 간격으로 요청을 예약하세요. 자동화에도 같은 캐시 타이밍이 적용됩니다. 휴일 캘린더는 읽기 사이에 며칠에서 몇 주를 기다려도 되고, 경제 지표 발표는 다음 예정 발표 때까지 캐시해 둘 수 있습니다. 요청이 몰릴 때는 분당 한도 안에 들도록 간격을 두세요.

상황별 계산 방법

상황적합한 방법이유
스크립트를 하루에 한 번 실행캐시 불필요스크립트가 이미 하루 간격으로 읽습니다
웹 서비스가 요청을 반복메모리 캐시 또는 공유 캐시데이터 종류별 TTL을 두면 반복 읽기가 요금에 잡히지 않습니다
요청이 한꺼번에 몰림요청 간격 두기분당 한도는 계속 적용됩니다

TTL은 응답을 얼마나 오래 재사용할 수 있는지를 정하고, 요청 간격은 호출이 API에 도달하는 시점을 조절합니다. 웹 서비스에는 둘 다 필요할 수 있습니다. 캐시는 같은 경로와 파라미터의 반복 읽기를 처리하고, 간격 조절은 그래도 API에 도달하는 호출을 다룹니다. 1시간짜리 캐시를 써도 경로나 파라미터가 다른 요청은 여전히 별도의 호출입니다. 다른 읽기가 캐시된 응답을 쓰더라도, 몰리는 요청에 간격 조절이 필요한지 판단할 때는 그 호출들도 포함하세요.

429나 402 응답도 요금이 청구되나요?

아니요, 오류에는 요금이 청구되지 않습니다. 429 요청 빈도 제한이든 잔액 부족의 402든 마찬가지입니다. 오류 본문은 application/problem+json 형식이며 status, code, detail, request_id를 포함합니다. 응답의 이유는 code로 알 수 있습니다.

잔액을 모두 썼다면 대시보드에서 크레딧을 충전하세요. 잠시 기다린다고 크레딧이 늘지 않으므로, 헬퍼는 402에서 재시도 루프를 계속하지 않고 오류를 발생시킵니다.

같은 주소를 공유하는 클라이언트는 함께 제한되나요?

네, 같은 주소를 공유하는 클라이언트는 함께 제한될 수 있습니다. 별도의 남용 방지 버킷이 하나의 네트워크 식별 단위에 분당 120건, 버스트 20건까지 허용합니다. 요청이 몰릴 때는 계정 한도와 이 공유 버킷을 모두 고려하세요.

그 밖에 어떤 오류를 재시도해야 하나요?

멱등 요청은 500이나 503 뒤에 한도가 있는 지수 백오프와 지터를 적용해 재시도하세요. 헬퍼는 시도 사이의 대기 시간을 늘리고 무작위 요소를 더하며, 한도가 있는 요청 루프가 끝날 때까지 성공한 응답이 없으면 오류를 발생시킵니다.

400은 재시도하지 말고, 401도 같은 키로 재시도하지 마세요. 404 series_not_found는 발표 이후에 조회할 수 있게 될 수 있으므로, 백오프 루프가 처리하는 서버 오류와 별도의 경우로 다루세요.

계산 예시: 대시보드 비용

1,000회 호출당 $2이면 $10의 무료 크레딧으로 한 달에 5,000회, 하루 약 166회를 호출할 수 있습니다. 15분마다 값 6개를 읽는 대시보드로 시작해 보겠습니다. 이 갱신 주기에서는 각 값을 하루에 96번 읽으므로, 6에 96을 곱하면 하루 576건의 요청이 됩니다.

다음으로 각 값을 1시간 동안 캐시합니다. 그 1시간 안의 대시보드 새로고침은 저장된 응답을 재사용하므로 API 요청은 하루 144건으로 줄어듭니다. 대시보드 비용이 원하는 수준을 넘는다면 새로고침 주기를 늘리는 방법도 있습니다.

계산일일 요청 수
$10의 무료 크레딧을 1,000회당 $2로 일 평균으로 환산약 166건
값 6개 × 15분 간격 새로고침 시 96회 읽기576
같은 값 6개를 각각 1시간 캐시한 경우144

에 게시되었습니다.

다음 단계