← 글 목록

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 (unset NOTION_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) — 배포 훅 또는 GitHub repository_dispatch

4.3 재빌드에 쓰는 이벤트

src/lib/notion-webhook.ts:

  • page.created · page.properties_updated · page.content_updated
  • page.deleted · page.undeleted · page.moved
  • data_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·태그 기준으로 보이는 건수이고, 홈 탭에 따로 두던 개발 메모도 여기로 이관해 둔 상태다.