세 가지 API를 위한 세 가지 sleep 간격: Steam 250ms, GitHub 100ms, HuggingFace 없음
요약
ETL 파이프라인 구축 시 Steam, GitHub, HuggingFace 등 서로 다른 API의 속도 제한(Rate Limit)을 관리하는 실무적인 전략을 다룹니다. 각 서비스의 특성에 맞춰 sleep 간격을 설정하고 오류를 처리하는 구체적인 사례를 제시합니다.
핵심 포인트
- Steam API는 명확한 문서가 부족하여 테스트를 통해 공격적인 250ms 간격 설정
- GitHub API는 인증(PAT)을 통해 실질적인 속도 제한 방어 수행
- HTTP 429 오류 발생 시 치명적이지 않은 데이터는 로그 기록 후 건너뛰는 예외 처리
- 작업 효율성과 API 제한 사이의 트레이드오프 고려 필요
지난 4월, 세 개의 프로그래밍 방식 디렉토리 사이트인 Top AI Tools (HuggingFace 데이터), Find Games Like (Steam 데이터), Open Alternative To (GitHub 데이터)를 위한 ETL 파이프라인을 구축할 때, 저는 같은 주에 완전히 다른 세 가지 API의 속도 제한 (rate limits)을 파악해야 했습니다. 수치, 실패 모드, 그리고 오류를 처리하는 올바른 방법이 모두 달랐습니다.
제가 실제로 적용한 내용과 각 수치 뒤에 숨겨진 논거는 다음과 같습니다.
Steam: 250ms, 의도적으로 공격적인 설정
Steam의 개발자 문서에는 명확한 속도 제한 (rate-limit) 세부 사항이 부족합니다. 커뮤니티 논의와 테스트를 통해 제가 알아낸 것은 다음과 같습니다: 공개 Web API의 경우 IP당 5분당 약 200개의 요청이 가능하며, 이는 문서상 안전한 간격으로 계산하면 1.5초당 1개의 요청이 됩니다. 제 코드에는 다음과 같이 명시적으로 주석을 달았습니다:
await sleep(250); // Steam 속도 제한: ~200/5분, 1.5초가 안전함; 250ms는 공격적이지만 보통 괜찮음
그럼에도 불구하고 제가 250ms를 선택한 이유는 ETL이 약 60개의 게임 항목에 대해 매일 밤 GitHub Actions 작업으로 실행되기 때문입니다. 250ms로 설정하면 총 15초의 sleep 시간이 소요됩니다. 1.5초로 설정하면 90초가 걸릴 것입니다. 크론 (cron) 작업이 처리해야 할 사이트가 세 곳이라는 점을 고려하면 이 차이는 중요합니다.
허용 가능한 리스크: Steam은 첫 번째 속도 제한 위반에 대해 즉각적인 차단을 하지 않습니다. 대신 HTTP 429를 반환하며, 작업 로그에 오류를 기록합니다. 게임 ETL은 리뷰 엔드포인트 (review-endpoint) 실패를 치명적이지 않은 것으로 처리합니다. 즉, 게임 행은 여전히 DB에 기록되지만, 다음 실행 전까지 리뷰 통계만 누락될 뿐입니다:
try {
const r = await getAppReviewSummary(appid);
// ... DB에 기록
...
reviewsFailed 카운터가 작업 로그에 나타납니다. 만약 이 수치가 지속적으로 상승하는 것을 본다면, 그것은 sleep 간격을 늘려야 한다는 신호입니다. 지금까지는 그럴 필요가 없었습니다.
GitHub: 100ms, 인증이 실질적인 역할을 수행함
GitHub의 REST API는 제한 사항을 명확하게 명시하고 있습니다: 인증되지 않은 경우 시간당 60회 요청, 개인 액세스 토큰 (Personal Access Token)을 사용하는 경우 시간당 5,000회 요청입니다. GitHub의 속도 제한 (Rate Limiting) 관련 문서는 기본 제한(Primary Limit)과 특정 엔드포인트 카테고리에 대한 보조 제한(Secondary Limits)을 모두 다룹니다. OSS 대안 ETL은 각 대안 프로젝트당 한 번의 GET /repos/:owner/:repo 호출을 수행하며, 이는 시드 데이터(Seed Data) 내의 SaaS 도구당 대략 3~5개의 리포지토리(Repo)에 해당합니다. 각 도구당 5개의 대안이 있는 50개의 도구로 대규모 시드 실행을 하더라도 요청은 250회에 불과합니다.
sleep은 예의를 지키기 위한 간격으로 존재하지만, 실제 속도 제한 방어는 인증(Authentication)이 수행하고 있습니다:
function authHeaders(): Record<string, string> {
const token = process.env.GITHUB_TOKEN;
const base: Record<string, string> = {
...
GITHUB_TOKEN은 리포지토리 비밀값(Repository Secret)으로부터 GitHub Actions에서 설정됩니다. 이 토큰이 없다면, 전체 시드 실행 시 시간당 60회의 요청 제한이 1분도 채 되지 않아 소진될 것입니다. 토큰이 있다면, 시간당 5,000회의 상한선 덕분에 여유로운 헤드룸(Headroom)을 확보할 수 있습니다.
한 가지 미묘한 차이점이 있습니다. GitHub에는 두 가지 별도의 속도 제한이 있습니다. 즉, 핵심 REST API 제한(인증 시 시간당 5,000회)과 검색 API(Search API) 제한(인증되지 않은 경우 분당 30회, 인증된 경우 초당 10회)입니다. 현재 ETL은 검색이 아닌 GET /repos/:owner/:repo를 직접 사용하므로, 더 완화된 핵심 제한이 적용됩니다. 만약 검색 기반의 탐색(Discovery) 방식으로 전환하게 된다면 계산 방식이 달라질 것입니다.
HuggingFace: 필요하지 않으므로 sleep 없음
모델 레지스트리 API(모델 목록 나열, 모델 메타데이터 가져오기)는 제가 몇 주 동안 매일 밤 실행하며 확인한 결과, 부딪힌 적이 없는 명시적인 속도 제한이 없습니다. ETL은 한 번의 GET /api/models?limit=100&sort=downloads 호출로 최대 100개의 모델을 가져온 다음, 모델당 한 번의 상세 정보를 가져옵니다. 100번의 빠른 요청에도 sleep이나 429(Too Many Requests) 오류는 발생하지 않았습니다.
이 중 일부는 인증된 요청(authenticated requests) 시 HUGGINGFACE_TOKEN 헤더를 사용하는 덕분이며, 이는 존재하는 모든 제한(ceiling)을 높여줍니다. 또 다른 이유는 레지스트리 API(registry API)가 배치 규모(batch scale)의 자동화 도구를 위해 명시적으로 설계되었기 때문입니다. 즉, 모델 카드(model cards), 메타데이터 스크래퍼(metadata scrapers), 리더보드 도구(leaderboard tools)들이 카탈로그를 소비하는 주요 방식입니다.
function authHeaders(): Record<string, string> {
const token = process.env.HUGGINGFACE_TOKEN;
return token ? { Authorization: `Bearer ${token}` } : {};
...
만약 매일 밤 가져오는 모델의 수를 1,000개로 확장한다면, 예방 차원에서 50ms의 sleep을 추가하겠습니다. 100개 규모에서는 작동하는 가장 단순한 방법이 곧 올바른 방법이기도 합니다.
비교
| API | Sleep | 인증 영향 (Auth impact) | 실패 모드 (Failure mode) | 치명적 여부 (Fatal?) |
|---|---|---|---|---|
| Steam appdetails | 250ms | 없음 (공개) | 429, 가끔 발생 | 치명적이지 않음 (Non-fatal) |
| ... |
네 가지 코드 경로 모두 치명적이지 않습니다(non-fatal). 배치(batch) 작업 중 어디에서든 429 오류나 연결 오류가 발생하면, Turso에 폴백 템플릿(fallback-template) 행을 작성하고 카운터를 증가시킵니다. 콘텐츠 업그레이드 루프가 다음 날 밤에 누락된 부분을 다시 채웁니다.
중요한 패턴
sleep 간격은 추측에 기반합니다. 속도 제한(rate-limit) 이벤트 이후에도 ETL이 무용지물이 되지 않도록 실제로 보호해 주는 것은 실패 비용이 저렴하다는 점입니다. 이 스택의 모든 외부 API 호출은 try/catch로 감싸져 있어, 배치를 중단시키는 대신 저하된 콘텐츠(degraded content)를 작성합니다. sleep 간격은 속도 제한에 걸릴 확률을 제어하며, 폴백 체인(fallback chain)은 실제로 속도 제한에 걸렸을 때 어떤 일이 일어날지를 제어합니다.
인디 규모의 ETL — 하룻밤에 수십 개에서 수백 개의 항목 — 의 경우, 다소 보수적인 sleep과 치명적이지 않은 오류 경로의 조합만으로 충분합니다. 만약 실행당 항목이 수천 개로 늘어난다면, 저는 두 가지를 모두 재고할 것입니다. 즉, 지수 백오프(exponential backoff)를 사용하는 큐 제한형 병렬 페처(queue-bounded concurrent fetcher)로 전환하고, 콘텐츠 생성과 데이터 가져오기를 독립적으로 재시도할 수 있는 단계로 분리하는 것입니다.
세 개의 AI 큐레이션 디렉토리 사이트를 운영하며 진행 중인 6개월간의 실험 중 일부입니다. 여기에 언급된 기술적 주장들은 실제이며, 이 기사는 AI의 도움을 받아 작성되었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기