BlueByte
HTTP 429Fixed

Notion API가 대량 쓰기에서 429 rate_limited를 반환

작성 Haneul Seo2026년 8월 30일 업데이트6 min

안녕하세요, BlueByte입니다. Notion 페이지를 많이 만들거나 갱신하는 스크립트가 429로 실패하기 시작했다면, 뭔가 잘못한 게 아닙니다 — 그저 Notion이 받아줄 수 있는 속도보다 빠르게 보내고 있을 뿐입니다. 오늘은 응답이 무엇을 말하는지 읽고, 세 가지 방법으로 클라이언트 속도를 낮춰 오류를 멈추는 순서로 짚어보겠습니다.

Notion의 429가 뜻하는 것

대량 작업이 이렇게 실패합니다:

{ "object": "error", "status": 429, "code": "rate_limited",
  "message": "You have been rate limited." }

부하가 심하면 code service_overload의 529도 보일 수 있습니다. 코드 입장에선 둘 다 같은 뜻입니다 — 물러서라. 순수하게 속도 문제라, 페이로드나 토큰이 잘못됐을 때는 다른 오류가 나므로 그 둘은 건드릴 필요가 없습니다.

대량 쓰기가 한도를 이렇게 빨리 넘는 이유

Notion은 연동 하나당 평균 초당 3요청으로 제한하고, 짧은 순간의 버스트는 그 위로 허용합니다. 제한은 연결(연동 토큰) 단위라, 한 토큰으로 병렬로 쏘는 요청들은 하나의 예산을 나눠 씁니다 — 병렬이 처리량을 늘려주는 게 아니라 천장에 더 빨리 닿게 할 뿐입니다. 페이지 생성 루프나 수백 건에 대한 Promise.all이면 거의 즉시 걸립니다.

무언가 바꾸기 전에 응답부터 읽기

어떤 한도에 걸렸는지 추측하지 마세요 — 응답이 알려줍니다. 429에는 Retry-After 헤더(대기 초)와 additional_data.rate_limit_reason 필드가 실립니다:

curl -si -X POST https://api.notion.com/v1/pages \
  -H "Authorization: Bearer $NOTION_TOKEN" \
  -H "Notion-Version: 2022-06-28" -H "Content-Type: application/json" \
  -d @page.json | grep -i "retry-after\|rate_limit_reason"

Retry-After가 있으면 속도 제한에 걸린 것입니다 — 토큰 오류(401)나 페이로드 오류(400)가 아닙니다. 이 헤더가 전혀 안 보이면 본문 전체를 로그로 남기세요. 거기 validation_error가 있으면 속도가 아니라 페이로드 문제입니다.

Retry-After를 지키고, 조절하고, 묶기

세 가지 변경을 싼 것부터 적용합니다.

  1. Retry-After를 지킵니다. 429가 나면 딱 그만큼 자고 재시도하세요 — 절대 즉시 재시도하지 마세요.
async function notionFetch(url, options, attempt = 0) {
  const res = await fetch(url, options);
  if (res.status === 429 && attempt < 5) {
    const retryAfter = Number(res.headers.get("retry-after") ?? 1);
    await new Promise((r) => setTimeout(r, retryAfter * 1000));
    return notionFetch(url, options, attempt + 1);
  }
  return res;
}
  1. 한도 아래로 조절합니다. 병렬 대신 ~350ms 간격으로 직렬 전송하세요. 초당 3요청 아래를 유지하면 429 자체를 거의 안 보게 됩니다.

  2. 작업을 묶습니다. 한 요청으로 최대 100개의 블록 자식을 500KB 페이로드 안에서 추가할 수 있습니다 — 블록마다 요청하지 말고 한 번의 호출로 묶으세요.

실제 사례: 1,000행 임포트

행 하나가 블록 10개짜리 페이지인 1,000행을 임포트한다고 합시다. 순진하게 하면 생성 1,000번 + 추가 수천 번이라 몇 초 만에 429가 납니다. 재구성하면, 각 페이지를 블록 10개와 함께 한 요청으로 생성하고(100블록 천장 훨씬 아래) 그 생성들을 ~350ms 간격으로 직렬 전송합니다. 이제 초당 약 3페이지로 429 없이 돌아가고, 혹 하나가 새어 나가도 Retry-After 핸들러가 기다렸다 이어가므로 임포트가 실패하지 않습니다.

작업이 깨끗이 도는지 확인

돌려보며 상태 코드를 지켜보세요. 조절과 Retry-After가 자리 잡으면 대부분 200이 흐르고, 많아야 스스로 회복하는 429가 가끔 보일 뿐 — 재시도 안 된 429나 529는 없어야 합니다.

페이로드·인증 오류와의 구분

400 validation_error는 페이로드 문제(블록 과다, 한도 초과 필드)라 재시도해도 소용없습니다. 401 unauthorized는 토큰·공유 문제입니다. 오직 429/529만 속도 문제이고, 그 둘만 Retry-After를 싣습니다. 조절과 재시도를 공유 클라이언트 한 곳에 넣어 두면 앞으로 어떤 코드 경로도 이걸 우회할 수 없고, 다음 대량 작업은 그냥 됩니다.

관련 질문

한도는 연동 단위인가요, 워크스페이스 단위인가요?

둘 다입니다. 평균 초당 3요청은 연결 단위이고, 그와 별개로 모든 연결이 공유하는 워크스페이스 전체 한도가 있으며 플랜에 따라 규모가 다릅니다.

429에 Retry-After 헤더가 없습니다. 얼마를 기다려야 하나요?

드문 경우지만, 짧은 지수 백오프(예: 1s, 2s, 4s)로 몇 회만 재시도하도록 대체하세요.

촘촘한 루프로 재시도하면 더 나빠지나요?

네. 속도 제한된 요청을 즉시 재시도하면 부하가 더해져 529 service_overload로 번질 수 있습니다. 항상 헤더의 대기 시간을 지키고 시도 횟수를 제한하세요.

한 요청에 블록을 몇 개까지 실을 수 있나요?

500KB 페이로드 안에서 블록 자식 최대 100개입니다. 이 천장까지 묶는 것이 요청 수를 줄이는 가장 큰 방법입니다.

연동 토큰을 여러 개 쓰면 처리량이 늘어나나요?

연결당 한도는 피하지만 워크스페이스 전체 한도는 못 피하고, 인증·감사가 복잡해집니다. 연동 하나를 조절·묶기로 다루는 게 더 단순하고 지원되는 방법입니다.

참고 자료

Haneul Seo

Infrastructure engineer · 10+ years running Linux fleets

같은 카테고리 다른 글

5.7.26Fixed

Gmail이 550-5.7.26으로 거부: 도메인의 DMARC 정책 때문에 인증되지 않은 메일이 수신 거부됨

Gmail이 우리 도메인이 직접 게시한 DMARC 정책을 집행한 결과입니다. 메시지가 header From: 도메인 기준으로 SPF·DKIM 정렬을 모두 통과하지 못했고, p=quarantine이나 p=reject 정책이 이를 하드 바운스로 바꿉니다. Authentication-Results 헤더가 어느 검사가 실패했는지 알려주고, SPF·DMARC·DKIM 레코드를 향한 dig 세 번이 원인을 지목합니다. SPF나 DKIM으로 인증하라는 비슷한 문구의 바운스는 정책이 아니라 Gmail의 기본 발신자 요구사항에 걸린 다른 문제입니다.

Gmail / Google Workspace
AADSTS50011Fixed

Microsoft Entra ID: AADSTS50011, 요청에 지정된 redirect URI 가 앱에 등록된 redirect URI 와 일치하지 않음

로그인은 끝까지 성공했는데 마지막 한 단계에서 Entra ID 가 거부합니다. 앱이 보낸 redirect_uri 가 등록된 URI 중 어느 것과도 문자 단위로 같지 않기 때문입니다. 비교는 대소문자를 구분하고 끝의 슬래시를 세며, localhost 를 제외하면 https 를 요구하고 포트도 일치해야 합니다. 목록도 web·spa·publicClient 세 개로 나뉘어 있고, application 객체가 아닌 service principal 에 넣은 URI 는 동기화 과정에서 사라질 수 있습니다. 오류 메시지에서 URI 와 앱 ID 를 꺼내 az ad app show 로 대조하고, az ad app update 나 Graph PATCH 로 정확한 문자열을 넣은 뒤 3~5분 기다리면 됩니다.

Microsoft Entra ID
535 5.7.139Workaround

Exchange Online: 535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled

smtp.office365.com 으로 메일을 보내던 스캐너·스크립트·앱이 535 5.7.139 로 멈추는 이유는 SMTP AUTH 프로토콜이 테넌트 전체, 해당 메일박스, 또는 Basic 인증을 막는 인증 정책·보안 기본값 중 어딘가에서 꺼져 있기 때문입니다. 문구(Tenant, Mailbox, 'did not meet the criteria')를 읽고 Get-TransportConfig·Get-CASMailbox·Get-AuthenticationPolicy 로 확인한 뒤, 테넌트 전체가 아니라 필요한 메일박스 하나만 엽니다. Basic SMTP AUTH 는 다리일 뿐입니다. Microsoft 는 2026년 12월 말 기존 테넌트에서 기본 비활성화하므로 발신자를 OAuth·High Volume Email·릴레이 커넥터로 옮겨야 합니다.

Exchange Online (Microsoft 365)
Error code: 5003Fixed

Zoom: "Unable to connect" 오류 코드 5003 — 브라우저는 되는데 데스크톱 앱만 Zoom 에 못 붙을 때

오류 5003 은 같은 PC 의 웹 클라이언트는 정상 입장하는데 Zoom 데스크톱 앱이 Zoom 서버와의 연결을 끝내지 못하는 상태입니다. 앱은 브라우저보다 더 많은 것을 필요로 합니다. Zoom 방화벽 문서는 미팅용 TCP 443/8801/8802 와 UDP 3478/3479/8801–8810, 인증서 검증용 CA 호스트 목록을 들고, zoom.us 와 *.zoom.us 를 프록시·SSL 검사에서 제외하라고 권고합니다. 포트 테스트, curl issuer 확인, 앱 내장 Network Connectivity Tool(Ctrl+Alt+Shift+D / Cmd+Option+Shift+D)로 어느 계층이 끊겼는지 보고 그 계층을 고치며, 재설치는 옆자리는 되는데 한 대만 실패할 때 씁니다.

Zoom
Slack cannot connect. / Last updated less than a minute ago…Fixed

Slack: 회사 프록시 뒤에서 뜨는 "Slack cannot connect"와 회색 "Last updated…" 띠

Slack은 채널을 평범한 HTTPS로 불러오지만 새 메시지는 443 포트로 Slack이 이름을 밝힌 wss-*.slack.com 호스트 셋(primary·backup·mobile)에 붙는 지속 WebSocket으로 받습니다. 프록시나 방화벽이 HTTP 쪽은 통과시키고 upgrade는 막으면 — 대개 wss 호스트에 SSL 복호화가 켜져 있거나 허용 목록이 slack.com에서 끝나기 때문에 — 브라우저는 멀쩡해 보이는데 앱은 회색 "Last updated…" 띠나 "Slack cannot connect."를 띄웁니다. 문제의 PC에서 curl 프로브 두 개로 어느 계층이 막혔는지 보고, wss 호스트 세 개를 복호화에서 예외 처리하고, my.slack.com/help/urls의 도메인을 전부 허용한 뒤 my.slack.com/help/test로 확인합니다.

Slack
locked for editingFixed

Word: 문서가 "locked for editing by another user"라며 열리지 않음

Word가 문서의 잠금(owner file)을 발견해 다른 누군가가 열어 두었다고 판단하고 읽기 전용 사본만 제안합니다. 대개는 아무도 열지 않았습니다: 크래시가 잠금을 남겼거나, 숨은 Word 프로세스가 여전히 파일을 쥐고 있는 것입니다. 어느 쪽인지 확인하고 Word 인스턴스를 모두 닫은 뒤 남겨진 ~$ owner file을 삭제하면 문서가 다시 편집 가능하게 열립니다.

Microsoft Word
Notion API가 대량 쓰기에서 429 rate_limited를 반환 · BlueByte