notes
Notion API 리미트 · 웹훅 · 캐시 최적화
Notion API 리미트·웹훅·캐시 최적화와 운영 시 주의점 메모.
블로그(former-force)에서 Notion API를 실행, rate_limited / HTTP 429가 난 이유를, 지금 레포에 있는 캐시·동시성·웹훅 기준으로 정리한 문서다.
관련: Untitled · Untitled · Untitled · 레포 cpt4567/former-force
1. 왜 리미트가 뜨나
Notion Public API는 연결(connection)당 평균 초당 약 3요청이다. 넘으면 code: rate_limited, HTTP 429, 헤더 Retry-After(초). 워크스페이스 플랜에 묶인 한도도 따로 있고, 그때도 같은 형태로 막힌다. 529(service_overload)도 잠깐 쉬었다가 같은 방식으로 재시도한다.
공식: Request limits
블로그 쪽이 특히 잘 터지는 지점은 본문 블록을 페이지마다 blocks.children.list로 재귀 fetch할 때다. Published가 200장 가까이 되면, 목록 쿼리만으로도 페이지네이션이 여러 번이고, 본문까지 붙이면 요청 수가 급격히 불어난다. NOTION_CACHE=0으로 디스크 캐시를 끄거나 .cache/notion을 지운 채 astro dev / astro build를 연속으로 돌리면 한도가 바로 보인다.
레포는 이 에러를 감싸서 이렇게 다시 던진다 (src/lib/notion.ts):
Notion API rate limited while …. Wait a minute, or reuse.cache/notion(unsetNOTION_CACHE=0).
2. 데이터 흐름 (한 장 요약)
토큰(NOTION_TOKEN)은 서버/빌드 env만. PUBLIC_*로 올리지 않는다.
3. 캐싱 최적화 (구현됨)
3.1 디스크 + 메모리
흐름은 단순하다. 같은 키로 이미 뜨는 요청이 있으면 inflight로 합치고, 메모리 → 디스크 순으로 보고, 둘 다 없거나 TTL이 지났을 때만 Notion을 친다.
3.2 동시성 캡
NOTION_MAX_CONCURRENT = 3 으로 Notion 호출을 큐잉한다 (withNotionLimit). 공식 평균 초당 3요청과 맞춰, 블록 재귀가 한꺼번에 폭주하지 않게 막는다.
3.3 목록은 본문을 안 가져온다
Home / /posts / /tags 는 getPublishedPostSummaries만 쓴다. 주석에도 적혀 있다 — 목록에서 블록 fetch를 빼야 rate limit과 Home 지연을 줄인다. 본문은 상세·빌드 풀 캐시 경로에서만 채운다.
3.4 풀 캐시와 목록의 어긋남
getPublishedPosts는 풀 캐시를 읽은 뒤, 요약 목록에만 있는 slug가 있으면 빠진 것만 증분 fetch해서 posts-full에 합친다. 예전에 본문 TTL이 목록보다 짧아 전체가 비는 문제를 줄이려는 쪽이다(결정 로그에 적힌 b79c690 계열).
3.5 당장 리미트가 났을 때
4. Notion 웹훅 (구현됨)
웹훅은 신호만 온다. 본문은 이벤트에 없고, “바뀌었다”는 알림을 받은 뒤 API로 다시 읽거나(또는 우리처럼) 캐시를 비우고 사이트를 다시 빌드한다.
공식: Webhooks events & delivery
4.1 엔드포인트
4.2 처리 순서
- 서명 검증 (
NOTION_WEBHOOK_VERIFICATION_TOKEN) - 이벤트 타입이 재빌드 대상인지 확인
- Posts DB / data source에 속한 변경인지 필터 (
eventTargetsBlogDatabase) clearNotionCache()— 메모리·디스크·dataSource 캐시 제거triggerSiteRebuild(event.type)— 배포 훅 또는 GitHubrepository_dispatch
4.3 재빌드에 쓰는 이벤트
src/lib/notion-webhook.ts:
page.created·page.properties_updated·page.content_updatedpage.deleted·page.undeleted·page.moveddata_source.content_updated·data_source.schema_updated
그 외 타입은 skipped: event_type으로 200 응답만 한다.
4.4 재배포 디바운스
src/lib/revalidate.ts:
훅/토큰이 없으면 triggered: false, configure DEPLOY_HOOK_URL or GITHUB_REBUILD_*.
4.5 Notion 쪽 설정 메모
- Integration에 Posts DB 연결
- Webhook subscription → 공개 URL
https://<host>/api/notion/webhook - verification token을
NOTION_WEBHOOK_VERIFICATION_TOKEN에 저장 - 이벤트는 수 분 안에 오지만,
page.content_updated등은 짧은 창에서 합쳐져 약간 늦을 수 있음 - 순서는 보장되지 않음 → 우리는 최신 전체를 빌드에서 다시 읽음
- 엔드포인트가 2xx가 아니면 Notion이 지수 백오프로 최대 약 8회 재시도(약 24시간 안)
5. env 표
6. 코드 맵
7. 아직 더 줄일 수 있는 것 (남은 개선)
이미 캐시·동시성·목록 분리·웹훅 디바운스는 들어가 있다. 그래도 Published가 계속 늘면 빌드 한 번에 본문 전량 fetch는 여전히 무겁다.
8. 운영 체크
- 로컬에서 리미트가 나면 캐시를 살린 채 재실행한다.
- 의도적 전체 갱신만
NOTION_CACHE=0또는.cache/notion삭제. - 프로덕션은 웹훅 → 캐시 클리어 → (디바운스된) 재배포가 정상 경로.
DEPLOY_HOOK_URL/GITHUB_REBUILD_*미설정이면 웹훅은 캐시만 지우고 빌드는 안 탄다 — 배포 env를 확인한다.- Posts 이관으로 Published가 크게 늘었으면, 첫 풀 빌드는 특히 캐시 워밍이 중요하다.
9. 블로그에 올라간 글은 어디서 관리하나
API·캐시·웹훅이 붙는 최종 글 목록은 Notion Posts DB다. Astro 빌드가 Published만 읽고, 웹훅도 이 DB(또는 그 data source) 변경만 본다.
운영할 때는 Default view 보드처럼 tags로 칸을 나눈다. CI/CD · ai · android · astro · docker · infra · ios · javascript … 칸마다 카드가 한 글이고, 여기서 제목·태그·Published를 고치면 다음 빌드(또는 웹훅 재배포)에 블로그에 반영된다. 관리자 UI는 없고, 글 쓰기·편집·공개 여부는 Notion만 쓴다.
위 화면이 그 Posts 보드다. 칸 숫자(예: infra 25, javascript 51)는 Published·태그 기준으로 보이는 건수이고, 홈 탭에 따로 두던 개발 메모도 여기로 이관해 둔 상태다.