ホーム / お役立ち記事 / Notion API が 429 で止まる理由|同時アクセスと Retry-After の待ち方

Knowledge

Notion API が​ 429 で​止まる​理由|​同時アクセスと​ Retry-After の​待ち方

Notion API が HTTP 429 を返して止まるのは、要求の数の上限に当たったためです。上限は接続ごと・ワークスペースごと・一部の操作ごとにあり、どれに当たったかで待つ時間が変わります。弊社が試した構成では、同じデータベースに別の処理もアクセスしていた間、トークンの確認は通るのに問い合わせだけが 429 になり、Retry-After は484秒でした。切り分け、待ち方、大きな本文の 403 の回避、確かめ方を、弊社の実測と公開の文書でまとめます。

著者: 村山 悠太(ゴムマリ) 最終更新: 2026年11月

Notion API を使う連携やスクリプトが、HTTP 429 を返して止まることがあります。429 は、要求の数が上限を超えたという応答です。

Notion の文書では、上限は接続ごと・ワークスペースごと・一部の操作ごとにあり、応答の Retry-After が、送り直すまでに待つ秒数を示します。

弊社が試した構成では、同じデータベースに別の処理もアクセスしていた間、トークンの確認は200で通るのに、データベースの問い合わせだけが429になりました。Retry-After は484秒でした。

直し方は、Retry-After の秒数を読み、それより長く待ってから送り直すことです。弊社の記録では、520秒を空けて送り直すと、1回目で通りました。

大きなコードブロックの更新が403で止まる件も、この記事で扱います。弊社が試した構成では、本文を gzip で圧縮して送ると通りました。

この記事は、Notion とほかのシステムをつなぐ仕組みを任されている業務の担当者・情報システムの担当者に向けて書きます。コードは、仕組みを作る人にそのまま渡せる形で載せます。

原因の切り分け、見分け方、待ち方、落とし穴、確かめ方を、弊社の実測と、Notion の公開の文書・HTTP の仕様(RFC 9110)で整理します。

原因: 429 は​要求の​数の​上限で、​上限は​3つの​単位に​ある

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日に確かめたものです。文書には、上限は今後変わりうる、という注もあります。

弊社の​実測: 問い​合わせだけが​429で、​Retry-After は​484秒だった

弊社は、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回目で通りました。このとき別の処理がまだ動いていたかは、記録していません。

見分け方​: 2回の​要求で、​どこが​止まっているかを​分ける

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時間に当たった上限が見られます。弊社はこの画面を確かめていません。

直し方​: Retry-After を​読んで​待ち、​送り直しを​1か所に​まとめる

手順は次のとおりです。弊社が実際に通したのは1と3で、2と4は Notion の文書の送り直しの決まりから足したものです。

  1. 429 の応答から Retry-After を読み、その秒数より長く待つ(弊社は520秒を空けた)
  2. 待つ間は、同じ処理から新しい要求を送らない
  3. 待った後に、止まった要求を送り直す
  4. 送り直す回数に上限を置き、上限に当たったら止めて、Retry-After と 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が返った道にそろえています。

大きな​コードブロックの​更新が​403に​なる​件: Cloudflare の​判定と​ gzip

弊社は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 が返ったら、送り直さずに止めます。

落とし穴: 実際に​踏んだ​もの

  • 10分ほど待っても、また429になった同じデータベースに別の処理がアクセスしている間は、待った後の送り直しも止まりました。文書のとおり、決めた秒数ではなく、応答の Retry-After を読んで待ちます。
  • 読み取りには、間が空いていなかった共有の仕組みは書き込みの間に0.35秒を空けていましたが、問い合わせなどの読み取りには空けていませんでした。
  • Cloudflare の HTML の403は、権限の問題ではなかった同じトークンの GET は通っていました。応答の本文が JSON か HTML かを、先に見ます。
  • 送り直しても、User-Agent を変えても、403のままだった通ったのは、本文を gzip で圧縮し、Content-Encoding: gzip を付けて送ったときでした。

確かめ方​: 2回の​要求と、​送り直しの​記録と、​読み戻し

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つです。

  • 同じデータベースを、複数の仕組みや人の作業が同時に読み書きしていて、どの要求が枠を使っているかが見えない
  • Retry-After が数分になると、待つ間に後ろの処理が待たされ、決めた時間に終わらなくなる
  • 403 のように、Notion の文書に無い動きに当たったときに、権限・大きさ・Cloudflare の判定のどれかを切り分ける手間がかかる

外注する​場合の​判断基準

外注の分かれ目は、同じ Notion に読み書きする仕組みが複数あり、要求の数と待ち方を全体で決め直す必要があるかです。外注するなら、見積もりを比べる前に次の3点を確かめてください。

  • 429 のときに Retry-After を読んで待ち、送り直す回数に上限を置く作りか
  • 同じデータベースを読み書きする処理の、頻度と順番を設計に入れるか
  • 受け入れを、件数と値の照合で行うか

ゴムマリでは、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週間〜)

システム連携・API 開発を見る

Related

関連

FAQ

よく​ある​質問

Notion API の Retry-After が、60秒より長くなることはありますか?

あります。Notion の文書では、接続ごとの上限に当たったときの Retry-After は最大60秒ですが、ワークスペース全体の上限では1分を超えることがある、とされています。弊社が試した構成では、データベースの問い合わせに484秒が返りました。

429 は、トークン全体が止められたということですか?

弊社が試した構成では、トークン全体の停止ではなかったと見ています。同じトークンで、ユーザーの情報を読む軽い要求は200で通り、データベースの問い合わせだけが429でした。まず軽い要求を1回送り、トークンの問題か、上限の問題かを分けます。

待って送り直しても、また 429 になります。

弊社の記録では、同じデータベースに別の処理がアクセスしていた間、10分ほど待って送り直しても、また429になりました。520秒を空けて送り直すと、1回目で通りました。Notion の文書は、送り直しを1か所にまとめるよう勧めています。

JavaScript の SDK を使えば、待ち方を書かなくてよいですか?

Notion の文書では、SDK は Retry-After を待って送り直しますが、待つ時間は既定で60秒が上限です。弊社が記録した484秒のような長い待ちに、既定のままで足りるかは、弊社は SDK で試していません。

大きなブロックを更新すると 403 になります。権限の問題ですか?

弊社が試した構成では、応答が JSON でなく Cloudflare の HTML の403は、権限の問題ではありませんでした。本文を gzip で圧縮し、Content-Encoding: gzip を付けて送ると200になりました。Notion の文書の権限の403は、JSON の restricted_resource で返ります。

ご相談は​こちらから

相談は無料です。

相談する