GitHub API rate limit, 여러 워커가 공유 캐시로 한도 아끼기
GitHub 인증 REST API는 2026년 7월 기준 토큰당 시간당 5,000회다. 워커 여러 개가 한 토큰을 쓰면 이 한도를 나눠 쓰므로, ETag를 담은 공유 캐시로 조건부 요청을 보내면 304 응답은 한도에서 빠진다.
목차
GitHub 공식 문서(REST API rate limits)에 따르면 2026년 7월 기준 인증된 REST API 요청 한도는 개인 액세스 토큰당 시간당 5,000회다. 이 한도는 프로세스가 아니라 토큰(사용자) 단위라, 워커를 여러 개 띄워 같은 토큰으로 호출하면 5,000회를 함께 나눠 쓴다. 해법은 ETag를 저장하는 공유 캐시를 두고 조건부 요청을 보내는 것이다. 리소스가 그대로면 GitHub이 304 Not Modified를 돌려주고, 이 304는 한도에서 빠진다. 이 글은 인증된 GitHub REST API를 여러 워커가 한 토큰으로 호출하는 상황에 한정하며, GraphQL API나 미인증 호출은 다루지 않는다.
왜 워커를 늘려도 한도는 그대로일까
한도가 프로세스가 아니라 토큰 단위이기 때문이다. GitHub 공식 문서는 미인증 요청을 시간당 60회, 개인 액세스 토큰으로 인증한 요청을 시간당 5,000회로 제한한다. 워커를 열 개로 늘려도 같은 토큰을 쓰면 열 프로세스가 이 5,000회를 함께 소진한다. 응답 헤더 x-ratelimit-remaining으로 남은 횟수를 확인하고, 이 값이 0이 되면 GitHub은 403이나 429를 돌려준다.
인증 방식마다 한도가 다르다. GitHub 공식 문서가 밝힌 값은 다음과 같다.
| 인증 방식 | 시간당 요청 한도 |
|---|---|
| 미인증 | 60회 |
| 개인 액세스 토큰 | 5,000회 |
| GitHub App 설치 토큰 | 5,000회(최소) |
| Enterprise Cloud 조직 설치 | 15,000회 |
그래서 처리량을 올리려고 워커 수만 늘리는 것은 한도 앞에서 의미가 없다. 늘려야 할 것은 한도에서 빠지는 요청의 비율이다.
조건부 요청은 어떻게 한도를 아끼나
변경이 없을 때 GitHub이 304를 돌려주고, 그 304가 한도 계산에서 빠지기 때문이다. GitHub 공식 문서(REST API 모범 사례)는 조건부 요청이 304 응답을 받고 Authorization 헤더로 올바르게 인증된 경우 기본 rate limit에 포함되지 않는다고 명시한다.
동작은 두 단계다. 먼저 응답 헤더의 ETag(리소스의 현재 버전을 가리키는 식별자) 값을 저장한다. 다음 요청 때 그 값을 If-None-Match 헤더에 실어 보낸다. 내용에 변화가 없으면 GitHub은 본문 없이 304만 돌려준다. ETag 대신 Last-Modified 값과 If-Modified-Since 헤더를 써도 결과는 같다. 단 조건부 요청은 GET 같은 안전한 메서드에만 걸린다.
# 1) 첫 요청에서 ETag를 받는다
curl -sI -H "Authorization: Bearer $TOKEN" \
https://api.github.com/repos/octocat/Hello-World | grep -i etag
# etag: W/"abc123..."
# 2) 받은 ETag를 If-None-Match로 보낸다
curl -sD - -o /dev/null \
-H "Authorization: Bearer $TOKEN" \
-H 'If-None-Match: W/"abc123..."' \
https://api.github.com/repos/octocat/Hello-World \
| grep -iE "HTTP|x-ratelimit-remaining"
# 변경이 없으면 HTTP/2 304, x-ratelimit-remaining 은 줄지 않는다두 번째 요청이 304를 받으면 남은 한도가 그대로다. 폴링처럼 같은 리소스를 반복 조회하는 작업이라면 대부분의 호출이 304로 떨어지고, 실제 한도 소진은 변경이 일어난 순간에만 발생한다.
공유 캐시로 워커들이 ETag를 나눠 쓰기
핵심은 ETag와 응답 본문을 워커마다 따로 들고 있지 말고 Redis 같은 공유 저장소 한 곳에 모으는 것이다. 그러면 한 워커가 받아 둔 ETag를 다른 워커가 그대로 재사용해 조건부 요청을 보낸다. 워커 열 개가 같은 리소스를 조회해도 실제 데이터 갱신은 한 번만 한도를 쓴다.
요청 한 번의 처리 순서는 이렇게 짠다.
- 요청 전에 공유 캐시에서 해당 URL의 항목(ETag와 본문)을 찾는다.
- 항목이 있으면 그 ETag를 If-None-Match에 실어 GitHub에 요청한다.
- 304가 오면 캐시의 본문을 그대로 쓴다. 한도 소진은 없다.
- 200이 오면 새 본문과 새 ETag로 캐시를 갱신한다. 이때만 한도를 1회 쓴다.
캐시 키 설계가 관건이다. ETag는 토큰별로 유효하므로 캐시 키에 리소스 URL뿐 아니라 토큰이나 설치 ID를 함께 넣어 토큰별로 항목을 분리하는 게 안전하다. 이 구조는 REST 리소스 캐싱의 일반적인 설계와 결이 같다. REST의 안전한 메서드와 리소스 표현 개념이 낯설다면 REST API의 뜻과 설계 원칙을 정리한 글을 먼저 보면 조건부 요청이 왜 GET에만 걸리는지 이해가 빠르다.
공유 캐시를 쓸 때 놓치기 쉬운 것
- ETag는 토큰별로만 유효하다. GitHub 공식 문서 기준 App 설치 토큰은 발급 후 1시간 뒤 만료되고, 토큰이 바뀌면 그 토큰으로 받아 둔 ETag는 더 이상 맞지 않는다. 토큰을 갱신할 때 관련 캐시 항목도 함께 무효화한다.
- 2차 rate limit은 따로 걸린다. GitHub 공식 문서는 REST와 GraphQL을 합쳐 동시 요청 100개, 단일 엔드포인트에 분당 900포인트를 넘기지 말라고 안내한다. 304가 기본 한도에서 빠져도 이 2차 제한에는 걸릴 수 있으니 워커 동시성을 제한하고 요청을 직렬로 보내는 편이 안전하다.
- 조건부 요청은 REST 전용이다. GraphQL API에는 ETag 기반 조건부 요청이 없다. GraphQL을 함께 쓴다면 그쪽은 별도의 포인트 기반 한도로 따로 관리한다.
- 304도 왕복은 한 번 일어난다. 공유 캐시는 한도와 본문 파싱 비용을 아끼지만 네트워크 요청 자체는 나간다. 갱신이 드문 리소스라면 캐시 항목에 짧은 신선도 시간(TTL)을 얹어, 그 시간 안에는 요청 없이 캐시 본문을 바로 쓰는 층을 더할 수 있다.
개념을 실제로 구현한 도구: octopool
지금까지의 조건부 요청과 공유 캐시 구조를 조직 단위로 구현한 오픈소스가 octopool이다. octopool 프로젝트에 따르면 이 도구는 MIT 라이선스의 TypeScript 프로젝트로, '조직 공유 GitHub 읽기 릴레이·캐시'를 단일 Cloudflare Worker 하나로 구현한다. 유지보수 팀과 봇이 gh pr view, gh pr checks, gh run list, gh issue list 같은 똑같은 read 형태를 반복하면서 primary rate limit을 빠르게 소진하는 문제를, 그 트래픽을 개별 머신에서 Cloudflare로 옮겨 푼다. 앞서 본 "한 토큰의 5,000회를 여러 프로세스가 나눠 쓴다"는 상황을 팀 전체 규모로 확장한 셈이다.
octopool 프로젝트가 밝힌 한도 절약 방식은 세 갈래다.
- 신원 풀링으로 버킷을 합산한다. 여러 PAT나 GitHub App 신원을 풀에 모아 각자의 rate 버킷을 합치고, Durable Object가 대상 리소스에 여유가 가장 많은 신원을 골라 요청을 태운다. 이때 짧은 sticky 리스를 걸어 여러 호출이 같은 신원으로 한꺼번에 몰리는 스탬피드를 막는다. 앞서 본 동시성·2차 한도 문제를 신원 선택 단계에서 함께 눌러 주는 구조다.
- 캐시 미스도 공개 경로를 먼저 시도한다. 캐시에 없는 요청은 먼저 토큰이 필요 없는 공개 GitHub 전송(공개 PR diff, compare patch, 워크플로 run list 등)을 시도하고, 그래도 안 되면 풀 신원으로 폴백한다. 신선한 엣지나 캐시 히트는 GitHub 쿼터를 전혀 쓰지 않는다. 조건부 요청의 304와 같은 결로, 인증 쿼터를 실제로 쓰는 호출 자체를 최대한 줄인다.
- 비밀은 Worker 안에만 둔다. PAT와 App 개인키는 Worker 시크릿으로만 존재하고 응답·로그·캐시 어디에도 노출되지 않는다.
접근 통제도 읽기 캐시라는 성격에 맞췄다. 검증된 조직 멤버만 caller 토큰을 발급받고, 대상은 공개 repo뿐이라 비공개 저장소는 각자 로컬 gh로 폴백한다. CLI는 gh 드롭인 shim이라 안전한 read만 octopool을 먼저 거치고, 뮤테이션이나 시크릿을 다루는 요청은 로컬 gh로 그대로 통과시킨다. 배포에는 Cloudflare Workers 유료 플랜(Durable Objects와 D1)과 검증된 org, 그리고 최소 한 개의 PAT나 App이 필요하다.
결국 처리량의 병목은 워커 수가 아니라 한도에서 빠지는 요청의 비율이다. GitHub도 반복 폴링 대신 웹훅으로 이벤트를 받으라고 권한다. 공유 캐시로 조건부 요청을 극대화하고, 그래도 자주 도는 조회는 웹훅으로 대체하는 것이 실무에서 쓰는 조합이다.

