ホーム / お役立ち記事 / Notion API が 429 で止まる理由|同時アクセスと Retry-After の待ち方
Knowledge
Notion API が HTTP 429 を返して止まるのは、要求の数の上限に当たったためです。上限は接続ごと・ワークスペースごと・一部の操作ごとにあり、どれに当たったかで待つ時間が変わります。弊社が試した構成では、同じデータベースに別の処理もアクセスしていた間、トークンの確認は通るのに問い合わせだけが 429 になり、Retry-After は484秒でした。切り分け、待ち方、大きな本文の 403 の回避、確かめ方を、弊社の実測と公開の文書でまとめます。
Notion API を使う連携やスクリプトが、HTTP 429 を返して止まることがあります。429 は、要求の数が上限を超えたという応答です。
Notion の文書では、上限は接続ごと・ワークスペースごと・一部の操作ごとにあり、応答の Retry-After が、送り直すまでに待つ秒数を示します。
弊社が試した構成では、同じデータベースに別の処理もアクセスしていた間、トークンの確認は200で通るのに、データベースの問い合わせだけが429になりました。Retry-After は484秒でした。
直し方は、Retry-After の秒数を読み、それより長く待ってから送り直すことです。弊社の記録では、520秒を空けて送り直すと、1回目で通りました。
大きなコードブロックの更新が403で止まる件も、この記事で扱います。弊社が試した構成では、本文を gzip で圧縮して送ると通りました。
この記事は、Notion とほかのシステムをつなぐ仕組みを任されている業務の担当者・情報システムの担当者に向けて書きます。コードは、仕組みを作る人にそのまま渡せる形で載せます。
原因の切り分け、見分け方、待ち方、落とし穴、確かめ方を、弊社の実測と、Notion の公開の文書・HTTP の仕様(RFC 9110)で整理します。
Notion の文書「Request limits」は、API に2つの上限があると書いています。接続(連携)ごとの上限と、ワークスペースごとの上限です。一部の操作には、それぞれの上限もあります。
| 上限 | 文書の記載 |
|---|---|
| 接続ごと | 60秒の枠で、Business・Enterprise は1分600回、ほかのプランは1分180回 |
| ワークスペースごと | 全部の接続で共有し、プランに応じた枠。回数は書かれていない |
| データソースの問い合わせ | 接続ごとに、1秒約3回の別の枠 |
待つ時間も、上限によって違います。接続ごとの上限の Retry-After は、枠が戻るまでの時間なので、最大60秒です。ワークスペースごとの上限の Retry-After は、1分を超えることがあります。
ワークスペースごとの上限は全部の接続で共有なので、1つの接続が自分の枠の中にいても、429 になりえます。
429 の応答の本文には、どの上限に当たったかが rate_limit_reason(additional_data の中)に入ります。文書は7つの値を挙げ、ほかの値も出うるとしています。
Retry-After の値は、HTTP の仕様(RFC 9110 の 10.2.3 節)では、日時か秒数のどちらかです。Notion の文書は、Notion の値は整数の秒数だと書いています。
文書の内容は、2026年11月1日に確かめたものです。文書には、上限は今後変わりうる、という注もあります。
弊社は、Notion のデータベースを読み書きする仕組みを作っています。その作業の中で、データベースの問い合わせが429で止まりました。
| 項目 | 内容 |
|---|---|
| 実測の日 | 2026年8月 |
| 同じ時間の様子 | 同じデータベースに、別の処理も並行してアクセスしていた |
| API の版 | 記録していない |
| トークンの確認 | 200 |
| データベースの問い合わせ | 429・Retry-After 484秒 |
トークンの確認は GET /v1/users/me で、トークンが生きているかを見る軽い要求です。同じトークンで、問い合わせ(POST /v1/databases/{id}/query)だけが429になりました。
Retry-After の484秒は、約8分です。弊社は、トークン全体ではなく、そのデータベースへの要求が止められていたと見ています。トークンの確認が通り、問い合わせだけが止まったためです。
このときの応答の rate_limit_reason は、記録していません。文書の上限のどれに当たったかは、記録からは言えません。
484秒は、文書が接続ごとの上限について書く最大60秒を超えています。1分を超えうると文書が書くのはワークスペースごとの上限で、問い合わせの別の枠の Retry-After の長さは、文書に書かれていません。
弊社が送ったのは、データベースの問い合わせです。文書の別の枠は「データソースの問い合わせ」について書かれていて、データベースの問い合わせに同じ枠がかかるかは、Request limits のページには書かれていません。
10分ほど待って送り直しても、別の処理が同じデータベースにアクセスしている間は、また429になりました。弊社は、待つ間も別の処理の要求が数えられ、制限が延びたと見ています。
Retry-After を読んで待つ作りで呼び出しを包み、520秒を空けて送り直すと、1回目で通りました。このとき別の処理がまだ動いていたかは、記録していません。
429 が出たら、まず軽い要求と、止まった要求を1回ずつ送ります。止まり方で、疑う所が変わります。
| 見え方 | 疑う所 |
|---|---|
| 軽い要求も、問い合わせも429 | 接続か、ワークスペースの上限 |
| 軽い要求は200で、問い合わせだけ429 | 問い合わせの側の上限(弊社の事例) |
| 401 | トークンが無効 |
| 403 で、本文が JSON | 権限か、ワークスペースのブロックの上限 |
| 403 で、本文が Cloudflare の HTML | Cloudflare の判定(下の節) |
401 と、JSON の403の意味は、Notion の文書「Status codes」の表のとおりです。JSON の403の code は restricted_resource で、本文の message が理由を書きます。
429 が出たら、Retry-After と一緒に rate_limit_reason を記録に残します。この記事のコードの diagnose は、2回の要求の結果と、この2つを出します。
文書は、rate_limit_reason が public_api_request_blocked のときだけは、送り直しても直らないので、Notion のサポートに連絡するよう書いています。
文書によると、接続の設定の Health のタブの「Rate limits」の一覧でも、過去24時間に当たった上限が見られます。弊社はこの画面を確かめていません。
手順は次のとおりです。弊社が実際に通したのは1と3で、2と4は Notion の文書の送り直しの決まりから足したものです。
rate_limit_reason を記録に残す文書は、決まった秒数を仮に置かず、返った値を使うよう書いています。Retry-After が無い応答だけは、指数で延ばす待ち(文書の例のコードでは上限30秒)に回します。
文書は、送り直しを HTTP を呼ぶ1か所にまとめ、処理ごとに別々の送り直しの輪を持たないよう勧めています。待ちが一斉に明けて、もう一度要求が集中するためです。
弊社の共有の仕組みは、書き込みの間に0.35秒を空けていましたが、問い合わせなどの読み取りには間を空けていませんでした。弊社は共有の仕組みを直さず、呼び出す側で待つ形にしました。
JavaScript の SDK は、文書によると Retry-After を待って送り直しますが、待つ時間は既定で60秒が上限です。弊社は SDK で試していないので、長い待ちの扱いは SDK の設定で確かめてください。
次のコードは、Notion API を Python で直接呼ぶときの呼び出しの部品です。標準のライブラリだけで動きます。
import gzip
import json
import random
import sys
import time
import urllib.error
import urllib.request
API = "https://api.notion.com/v1"
HDRS = {"Authorization": "Bearer <トークン>", "Notion-Version": "2022-06-28",
"Content-Type": "application/json"}
GZIP_OVER = 50_000 # これより大きい本文は gzip で送る(弊社の実装の値)
MAX_TRIES = 6 # 1回目を含めて送る回数の上限
MARGIN = 5 # Retry-After に足して待つ秒数
_resume_at = 0.0 # この時刻(time.monotonic)まで、次の要求を送らない
def _reason(raw):
"""429 の本文から、当たった上限の名前(rate_limit_reason)を取る。"""
try:
return (json.loads(raw).get("additional_data") or {}).get("rate_limit_reason", "")
except ValueError:
return ""
def _wait_seconds(err, attempt):
"""Retry-After が整数の秒数なら MARGIN を足し、無ければ指数で延ばす(上限30秒)。"""
ra = (err.headers.get("Retry-After") or "").strip()
base = int(ra) + MARGIN if ra.isdigit() else min(2 ** attempt, 30)
return base + random.uniform(0, 1)
def api(method, path, body=None):
global _resume_at
data = json.dumps(body, ensure_ascii=False).encode("utf-8") if body is not None else None
hdrs = dict(HDRS)
if data is not None and len(data) > GZIP_OVER:
data = gzip.compress(data)
hdrs["Content-Encoding"] = "gzip"
for attempt in range(MAX_TRIES):
pause = _resume_at - time.monotonic()
if pause > 0: # 前の 429 の待ちが残っていれば、送る前に待つ
time.sleep(pause)
req = urllib.request.Request(API + path, data=data, headers=hdrs, method=method)
try:
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", "replace")
if "Cloudflare" in raw: # Notion の JSON でなく Cloudflare の HTML。送り直さない
sys.exit(f"NG HTTP {e.code} {method} {path}: Cloudflare 403(WAF)")
reason = _reason(raw)
if e.code not in (429, 529) or reason == "public_api_request_blocked":
sys.exit(f"NG HTTP {e.code} {method} {path}: {raw[:300]}")
if attempt == MAX_TRIES - 1:
sys.exit(f"NG HTTP {e.code} {method} {path}: {MAX_TRIES}回送って通らない"
f"(Retry-After {e.headers.get('Retry-After')}・{reason})")
wait = _wait_seconds(e, attempt)
print(f"HTTP {e.code} {method} {path}: {reason} {wait:.0f}秒待って送り直す", file=sys.stderr)
_resume_at = time.monotonic() + wait
def diagnose(db_id):
"""① トークンが生きているか ② そのデータベースの問い合わせが止まっているかを、1回ずつ見る。"""
for method, path, body in (("GET", "/users/me", None),
("POST", f"/databases/{db_id}/query", b'{"page_size": 1}')):
req = urllib.request.Request(API + path, data=body, headers=HDRS, method=method)
try:
with urllib.request.urlopen(req) as r:
print(method, path, r.status)
except urllib.error.HTTPError as e:
raw = e.read().decode("utf-8", "replace")
print(method, path, e.code, "Retry-After:", e.headers.get("Retry-After"),
"reason:", _reason(raw) or ("Cloudflare の HTML" if "Cloudflare" in raw else raw[:120]))
api は、429 と529(文書では、Notion が一時的に過負荷のときの応答)のときだけ、待って送り直します。ほかの誤りは、送り直さずに止めます。
文書は、500・502・503・504を、GET・DELETE のような繰り返しても結果が変わらない要求に限って(アプリに独自の守りがある場合を除いて)送り直すよう書いていますが、このコードは扱いません。
文書の手順は、続けて429が来たら待ちを指数で延ばすとしています。このコードは、文書の例のコードと同じく、Retry-After がある間はその秒数で待ちます。
_resume_at は、同じ処理の中のすべての呼び出しで共有する待ちです。1つの呼び出しが429を受けたら、ほかの呼び出しも、その時刻まで送りません。
大きな本文を gzip で送る条件と、Cloudflare の HTML が返ったら止める条件は、弊社の実装の呼び出しの部品と同じです。この2つは、下の403の節で書きます。
429 の送り直しの部分は、この記事のために書いたものです。弊社の記録にあるのは、Retry-After を読んで待つ作りで包んだことと、その結果で、元のコードを縮めたものではありません。
足したものは、共有の待ち(_resume_at)、rate_limit_reason の読み取り、public_api_request_blocked で止める条件、529の扱い、回数の上限、diagnose です。
diagnose は、弊社の記録にある切り分けの手順(軽い要求、次に問い合わせ)を形にしたものです。ほかは Notion の文書の決まりに沿っています。
Notion-Version の値は、弊社の実装のものです。diagnose の問い合わせの道(/databases/{id}/query)も、弊社の実測で429が返った道にそろえています。
弊社は2026年9月に、お客様の Notion のページで、約8.6万字のコードブロックを更新しました(PATCH /v1/blocks/{id})。
本文の文字は2,000字までの上限に合わせて40あまりに分け、送った JSON は約120KBでした。
応答は HTTP 403 で、本文は JSON でなく、Cloudflare の「Attention Required!」の HTML でした。
同じ内容の送り直し、curl での送信、User-Agent をブラウザ風にした送信も、どれも403でした。同じ時に、約1.4万字の小さいブロックの同じ種類の更新は、200で通りました。
2026年8月には、同じ大きさの更新が通っていました。弊社は、Cloudflare の判定(WAF のルールか、送り元の IP の評価)が変わったと見ていますが、どちらかは確かめていません。
本文の大きさは、文書の上限の内側です。Notion の文書は、1回の要求の本文の上限を500KB・ブロックの要素1,000としています。
弊社が2026年11月に試すと、本文が約604KBの要求は、400の validation_error でなく413で拒まれました。
権限の403とも違います。Notion の文書の403は、JSON の restricted_resource で、トークンの権限不足か、ワークスペースのブロックの上限を表します。
RFC 9110 の 15.5.4 節も、403 は、認証の情報と関係のない理由でも返りうる、と書いています。
回避は、本文を gzip で圧縮し、Content-Encoding: gzip を付けて送ることでした。200が返り、読み戻した全文が送った文と一致し、ほかのブロックは変わっていませんでした。
RFC 9110 の 8.4 節は、Content-Encoding を、本文にかけた圧縮などの符号化を示す欄と定めています。受け取る側は、受け付けない符号化なら415を返してよい、とされています。
Notion がリクエストの本文の gzip を受け付けることは、この記事で読んだ Notion の文書(Request limits・Status codes)には書かれていません。弊社が試した構成での動きです。
なぜ圧縮すると通るのかは、切り分けていません。弊社の実装は、50KBを超える本文を既定で圧縮し、Cloudflare の HTML が返ったら、送り直さずに止めます。
Content-Encoding: gzip を付けて送ったときでした。429 が直ったかは、diagnose で、トークンの確認と問い合わせを1回ずつ送って見ます。両方が200なら、その時点では止まっていません。
送り直しを入れた後は、処理の記録で、何秒待って送り直したかと、送り直した後に200になったかを見ます。上のコードの api は、待つたびに1行を出します。
送り直しが回数の上限に当たって止まったら、出た Retry-After と rate_limit_reason から、見分け方の表に戻ります。
403 の回避が効いたかは、更新したブロックを読み戻して見ます。弊社は、読み戻した全文が送った文と一致すること、ほかのブロックが変わっていないことを確かめました。
上のコードは、2026年11月1日に Python 3.14.6 で、作り物の応答を返す部品に差し替えて確かめました。Notion の本物の API には送っていません。
確かめたのは、Retry-After の484秒より長く待って1回で通ること、Retry-After が無いときに短く延ばすこと、送り直しの上限で止まることです。
public_api_request_blocked と、JSON の403と、Cloudflare の HTML の403では、送り直さずに止まることも確かめました。
50KBを超える本文だけが gzip で送られ、解凍すると元の JSON に戻ることも見ました。
1本の仕組みが、1つのデータベースを読み書きするだけなら、自力で足ります。この記事の api のように、Retry-After を読んで待つ作りを、HTTP を呼ぶ1か所に入れます。
自力の壁は次の3つです。
外注の分かれ目は、同じ Notion に読み書きする仕組みが複数あり、要求の数と待ち方を全体で決め直す必要があるかです。外注するなら、見積もりを比べる前に次の3点を確かめてください。
ゴムマリでは、Notion とほかのシステムをつなぐ連携の設計(どの項目を・どの向きに・どの頻度で)・構築・受け入れの照合(件数・値の一致)・保守を、システム連携・API 開発として承ります。
料金は、連携1本(接続先2つ・片方向・定期の更新)で初期50万円から、保守は月3万円から、期間は2週間からです。進め方は相談からご連絡ください。
Notion で頼めることの全体は、Notion の構築と開発にまとめています。
止まった処理の記録を1つ開き、429 の応答の Retry-After を見てください。
1分を超えるなら、文書の上では接続ごとの上限ではありません。60秒以下でも接続ごとの上限とは限らないので、rate_limit_reason もあわせて見ます。
そのうえで、diagnose で軽い要求と問い合わせを1回ずつ送り、見分け方の表で止まっている所を分けます。直した後は、送り直しの記録で、待った後に200になったかを見ます。
Notion への同期が、消した値を前の値のまま残す件は、Salesforce から Notion への同期で消した値が残る理由にまとめています。そちらは、要求が通った後の、書いた値の正しさの層です。
関連記事: Salesforce から Notion への同期で消した値が残る理由/Notion AI の自動入力が直した値を上書きする理由
ワークスアプリケーションズで大手企業向けERPの営業と導入を担当し、外資系SaaS企業と国内大手放送局で販路と新規事業の立ち上げ、AI学習用データを扱うAIスタートアップ2社でマーケティングと営業を担った。2024年10月から生成AIの実装に入り、いまは大手企業の現場で、AIエージェントを要件定義から運用まで手がけている。
Ask
業種・規模を問わず
システム連携・API 開発
SAP® ソフトウェアや基幹システム、intra-mart、SharePoint などのデータを、Notion や CRM などほかのシステムへつなぎます。
初期 50万円〜(2週間〜)
Related
Excel の台帳を Notion へ移すには、表の形とキーを決め、型を決めてリレーションと一緒に取り込み、数式と集計を作り直してから、件数・キー・値・先頭のゼロ・空の値・リレーション・計算の結果の7つで照合します。Notion の公式ヘルプと API の文書の決まりと、弊社が管理表の約2千明細を移し、Notion で計算した残が移したすべてのロットで元の表と一致した照合の手順でまとめます。
活用ガイドNotion AI に書類を作らせて照合するときは、手順を Notion AI への指示書、読めば決まる処理を添付のコード、1回ごとの結果を記録のデータベースに分けます。弊社の導入の事例の試作では、同じ題材で、文章の指示は発注番号を3回のうち2回誤り、添付のコードは2回とも誤りませんでした。分け方の問い、進める手順、誤りを仕込む照合の進め方を、弊社の記録でまとめます。
FAQ
あります。Notion の文書では、接続ごとの上限に当たったときの Retry-After は最大60秒ですが、ワークスペース全体の上限では1分を超えることがある、とされています。弊社が試した構成では、データベースの問い合わせに484秒が返りました。
弊社が試した構成では、トークン全体の停止ではなかったと見ています。同じトークンで、ユーザーの情報を読む軽い要求は200で通り、データベースの問い合わせだけが429でした。まず軽い要求を1回送り、トークンの問題か、上限の問題かを分けます。
弊社の記録では、同じデータベースに別の処理がアクセスしていた間、10分ほど待って送り直しても、また429になりました。520秒を空けて送り直すと、1回目で通りました。Notion の文書は、送り直しを1か所にまとめるよう勧めています。
Notion の文書では、SDK は Retry-After を待って送り直しますが、待つ時間は既定で60秒が上限です。弊社が記録した484秒のような長い待ちに、既定のままで足りるかは、弊社は SDK で試していません。
弊社が試した構成では、応答が JSON でなく Cloudflare の HTML の403は、権限の問題ではありませんでした。本文を gzip で圧縮し、Content-Encoding: gzip を付けて送ると200になりました。Notion の文書の権限の403は、JSON の restricted_resource で返ります。