rate_limit_eventでMessageParseErrorが発生し停止する原因と対処
Python版Claude Agent SDKがrate_limit_eventでMessageParseErrorを起こしreceive_messages()が停止する既知バグの原因と対処をまとめます。
rate_limit_eventでMessageParseErrorが起きるとどうなるか
Python版のClaude Agent SDK(claude-agent-sdk)には、Claude Code CLIがrate_limit_eventというメッセージ型を送ってきた瞬間にMessageParseErrorを投げ、ClaudeSDKClientのreceive_messages()が使う非同期ジェネレータそのものを停止させるバグが過去に存在しました。エラーが起きるのはyield式の内側なので、ジェネレータは再開できず、以降どんなメッセージも受け取れなくなります。セッション自体は生きたまま、アプリケーション側だけがメッセージを受信できなくなるのが特徴です。
このバグはclaude-agent-sdk-python issue #583として報告され、SDK 0.1.40でクラッシュ自体は修正済みです。その後も継続的にリリースが重ねられ、0.1.40から数えて100バージョン以上が公開されています。requirements.txtでバージョンを固定している環境や、古いDockerイメージを使い続けている環境では今も起きます。CLI側がrate_limit_eventのような新しいメッセージ型を送り始めたタイミングで、固定していたSDKのバージョンだけが取り残されるためです。rate_limit_event自体は、サブスクリプション経由の利用がプランの利用上限に近づいたときにCLIが送る状態通知で、詳しい仕組みはClaude Agent SDKはプランの利用上限を消費するかで扱っています。
receive_messages()が停止する仕組み
原因はmessage_parser.py内のparse_message()が、メッセージ型をmatch文で厳格に列挙していたことにあります。issue報告時点のコードは次のような構造でした。
match message_type:
case "user": ...
case "assistant": ...
case "system": ...
case "result": ...
case "stream_event": ...
case _:
raise MessageParseError(f"Unknown message type: {message_type}", data)rate_limit_eventはこの許可リストに含まれていなかったため、CLIがこの型のメッセージを送った瞬間にcase _へ落ち、MessageParseErrorが発生していました。この例外はclient.pyのreceive_messages()内でyield parse_message(data)という形で呼ばれており、yieldに渡す式の評価中に例外が発生すると非同期ジェネレータは終了状態に遷移します。呼び出し側のasync for message in client.receive_messages():はその例外をそのまま受け取り、囲むtry/exceptが無ければプログラム全体がその場でクラッシュします。try/exceptで捕まえていた場合でも、ジェネレータ自体は終了済みで再開できないため、以降のメッセージは二度と届きません。
再現条件とエラーメッセージ
issueの再現コードはClaudeAgent(options) / ClaudeAgentOptions(workdir=".")という当時のクラス名・引数名で書かれています。以下は現行の公開API(ClaudeSDKClient / cwd)に置き換えて示す、同じ状況を再現するコードです。connect()後にreceive_messages()でメッセージを回している最中に、CLIがレート制限の状態変化を通知するrate_limit_eventを送ってくると発生します。
import asyncio
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(cwd=".")
client = ClaudeSDKClient(options)
await client.connect()
await client.query("Hello")
async for message in client.receive_messages():
print(message)
# レート制限の状態が変化してCLIがrate_limit_eventを送ると、
# ここでMessageParseErrorが発生しジェネレータが終了する。
# 以降このループには二度と入らない。
asyncio.run(main())このとき投げられる例外はMessageParseError("Unknown message type: rate_limit_event", data)です。issue報告時の環境はclaude-agent-sdk==0.1.37 / Claude Code CLI 2.1.45でした。CLI側が新しいメッセージ型を送り始めたタイミングとSDKの対応が噛み合わなかったことが引き金です。
なぜジェネレータの停止が特に深刻か
このバグが厄介なのは、レート制限の通知自体は本来致命的な情報ではない点です。rate_limit_eventは状態変化を知らせるだけの補助的な通知で、無視してもセッションの実行に支障はありません。にもかかわらず、SDK側の型チェックの厳格さがそれを致命的なクラッシュに変えていました。しかもこの通知は、実際にレート制限へ近づいたときだけ送られるわけではありません。OAuth認証(サブスクリプション経由)のセッションでは、開始直後からほぼ毎回rate_limit_eventが届きます。「レート制限に達していないのにMessageParseErrorが出る」という一見矛盾した状況は、この通知の性質を知らないと原因が読めません。
もう一つの問題は再発性です。許可リストに無い型を例外にする実装は、CLIが将来新しいメッセージ型を追加するたびに同じクラッシュを引き起こします。rate_limit_eventは氷山の一角で、CLIとSDKのバージョンが噛み合わない限り同種の障害が起き得る設計でした。
背景には、Pythonの非同期ジェネレータの仕様もあります。async forで回しているジェネレータの内部で例外が送出されると、その例外は呼び出し元まで伝播したうえで、ジェネレータ自体がStopAsyncIteration相当の終了状態に遷移します。tryで例外を握りつぶしてcontinueしても、ジェネレータの実行フレームは一度壊れると元には戻りません。つまり呼び出し側のループをtry/exceptで囲んでも、次のasync forの反復に進めるわけではなく、ループそのものが終わります。この性質を知らずに「例外を捕まえれば継続できるはず」と実装してしまうと、ワークアラウンドとして機能しない対処に時間を使うことになります。
アップグレード後に得られるレート制限情報
0.1.40はクラッシュを止めるだけで、rate_limit_eventの中身はlogger.debugにログを残してスキップされ、まだ読み取れません。rate_limit_eventをRateLimitEvent型として正しくパースし、レート制限の状態変化を把握できる情報源にする変更は0.1.49で入りました(CHANGELOG「RateLimitEvent: Added typed RateLimitEvent message (#648)」)。現在の実装では、RateLimitEventの中にRateLimitInfoというデータクラスが入っており、次のフィールドを読み取れます。
| フィールド | 内容 |
|---|---|
status | 内容現在のレート制限状態。allowed(問題なし)・allowed_warning(上限に接近中)・rejected(上限到達)のいずれか |
resets_at | 内容レート制限ウィンドウがリセットされるUnixタイムスタンプ |
rate_limit_type | 内容適用されているウィンドウの種類。five_hour・seven_day・seven_day_opus・seven_day_sonnet・overageのいずれか |
utilization | 内容ウィンドウの消費割合(0.0〜1.0の小数) |
overage_status | 内容従量課金(overage)を使う場合のステータス |
overage_resets_at | 内容overageウィンドウのリセット時刻 |
overage_disabled_reason | 内容statusがrejectedのときにoverageが使えない理由 |
statusがallowed_warningに変わったタイミングでユーザーに警告を出す、rejectedになったら自前でバックオフして再試行する、といった制御をアプリケーション側で組めるようになります。0.1.40のクラッシュ修正とは別に、0.1.49でSDK利用者がレート制限の接近を能動的に検知できるようになった変更です。レート制限の情報を実際に使いたい場合は、下限を>=0.1.49にする必要があります。
恒久的な対処 — SDKを更新する
公式の修正は2段階に分かれています。まず0.1.40で、未知のメッセージ型を例外にせず黙ってスキップする実装に変更しました。次に0.1.49で、rate_limit_eventだけを専用の型としてパースするようにしました。CHANGELOGの該当エントリです。
| バージョン | 挙動 |
|---|---|
| 0.1.39以前 | 挙動未知のメッセージ型(rate_limit_event含む)を受けるとMessageParseErrorを送出し、receive_messages()が終了する |
| 0.1.40〜0.1.48 | 挙動クラッシュはしなくなったが、rate_limit_eventを含む未知の型はlogger.debugでログを残しつつNoneを返すだけで、中身は読めない(ジェネレータは継続) |
| 0.1.49以降 | 挙動rate_limit_eventをRateLimitEvent型として正しくパースできる(それ以外の未知の型は引き続きNoneを返す) |
現在のmessage_parser.pyを見ると、rate_limit_eventは次のように専用のデータクラスへパースされるようになっています。
case "rate_limit_event":
try:
info = data["rate_limit_info"]
return RateLimitEvent(
rate_limit_info=RateLimitInfo(
status=info["status"],
resets_at=info.get("resetsAt"),
rate_limit_type=info.get("rateLimitType"),
utilization=info.get("utilization"),
overage_status=info.get("overageStatus"),
overage_resets_at=info.get("overageResetsAt"),
overage_disabled_reason=info.get("overageDisabledReason"),
raw=info,
),
uuid=data["uuid"],
session_id=data["session_id"],
)
except KeyError as e:
raise MessageParseError(
f"Missing required field in rate_limit_event message: {e}", data
) from e
case _:
# Forward-compatible: skip unrecognized message types so newer
# CLI versions don't crash older SDK versions.
logger.debug("Skipping unknown message type: %s", message_type)
return Noneissueが報告されてから修正版が出るまでの流れは次のとおりです。
| 日付 | 出来事 |
|---|---|
| 2026-02-16 | 出来事claude-agent-sdk 0.1.37リリース(issue報告時の環境) |
| 2026-02-17 | 出来事issue #583報告。rate_limit_eventによるクラッシュと原因・修正案を提示 |
| 2026-02-19 | 出来事0.1.39リリース(この時点では未修正) |
| 2026-02-24 | 出来事0.1.40リリース。未知の型を非致命的にする修正が反映(クラッシュ自体はここで解消) |
| 2026-03-17 | 出来事0.1.49リリース。rate_limit_eventの専用パース(RateLimitEvent型)が反映 |
クラッシュを止めるだけなら報告から1週間程度でしたが、rate_limit_eventの中身まで読み取れるようにするには0.1.49までさらに1か月弱かかっています。対処自体は単純で、pip install --upgrade claude-agent-sdkで更新することです。
pip install --upgrade "claude-agent-sdk>=0.1.49"
python -c "from claude_agent_sdk import __version__; print(__version__)"インストール済みのバージョンを確認してから固定バージョンの指定を外すか、下限を緩めるのが安全です。クラッシュを止めるだけなら>=0.1.40で足りますが、レート制限の情報まで使う場合は>=0.1.49が下限になります。
更新できない場合の回避策
依存関係の都合ですぐにアップグレードできない場合、issue報告者が提案した回避策があります。parse_messageを差し替えて、MessageParseErrorのうち「Unknown message type」を含むものだけNoneにする、というモンキーパッチです。非公式の暫定策であり、公式のサポート対象ではない点に注意してください。
import claude_agent_sdk._internal.message_parser as mp
_original = mp.parse_message
def _tolerant_parse(data):
try:
return _original(data)
except mp.MessageParseError as e:
if "Unknown message type" in str(e):
return None
raise
mp.parse_message = _tolerant_parse
# client.pyがparse_messageを直接importして保持しているため、
# そちらの参照も同時に差し替える必要がある
import claude_agent_sdk.client as client_mod
if hasattr(client_mod, "parse_message"):
client_mod.parse_message = _tolerant_parseこの回避策はNoneを返すだけで、rate_limit_eventの中身(レート制限のステータスや残り時間)は読み取れません。あくまでクラッシュを止めるための応急処置で、レート制限の状態を実際にアプリケーション側で使いたい場合は素直にSDKを更新したほうが得られる情報が多くなります。
適用先が0.1.39以前の場合、receive_messages()自体はyield parse_message(data)という形で戻り値をそのままyieldするだけで、Noneを弾くチェックを持っていません。そのため、このパッチを当てるとNoneが呼び出し側のasync for message in client.receive_messages():にそのまま届きます。呼び出し側のループにif message is None: continueを追加しておかないと、Noneを通常のメッセージとして扱おうとしてAttributeErrorなどの別の例外に化けます。
なお条件判定が"Unknown message type" in str(e)という文字列一致になっているため、この暫定パッチはrate_limit_eventに限らず「許可リストに無い未知の型」全般をNoneで無害化します。将来CLIが別の新しいメッセージ型を追加した場合にも、同じクラッシュを一時的に防げる保険として働きます。
parse_message()を直接呼んでいるコードは戻り値の変化に注意
receive_messages()経由でSDKを使っているだけなら意識する必要はありませんが、message_parser.parse_message()を自分のコードから直接呼び出している実装がある場合は、0.1.40の修正が関数のシグネチャそのものを変えている点に注意が必要です。0.1.39以前は「Messageを返すか、MessageParseErrorを送出するか」の二択でした。0.1.40以降は戻り値の型がMessage | Noneになり、未知の型では例外を送出せずNoneを返します。
直接呼び出しているコードがparse_message()の戻り値を無条件にMessageとして扱っている場合、更新後はNoneが渡ってきてAttributeErrorなど別の例外に化ける可能性があります。0.1.40以降のreceive_messages()自身はif message is not None: yield messageという形でこのNoneを吸収していますが、独自のラッパーを書いている場合は同じチェックを追加する必要があります。
この症状かどうかを最短で確認する方法
receive_messages()やreceive_response()が途中で反応しなくなる原因は複数あり、CLI起動失敗やプロセス終了系のエラーと見分けが付きにくいことがあります。次の2点で本記事のバグかどうかを切り分けられます。
- スタックトレースの中身を見る。例外の型が
MessageParseErrorで、メッセージにUnknown message type: rate_limit_eventという文字列を含んでいれば本記事の症状です。CLINotFoundErrorやProcessErrorとは例外の型自体が異なるため、ログさえ残っていれば取り違えません - インストール済みバージョンを確認する。
pip show claude-agent-sdkで表示されるバージョンが0.1.39以前なら、CLI側がrate_limit_eventを送信するタイミングでほぼ確実にこのバグを踏みます。0.1.40以上であれば別原因を疑う必要があります
エラーログが既に失われている場合でも、バージョンが0.1.39以前であればアップグレードしておく価値はあります。0.1.40以降は100以上のリリースを経ており、古いバージョンを使い続けるメリットはほぼありません。SDK側のバージョンだけでなく、claude --versionで確認できるCLI側のバージョンも合わせて記録しておくと、社内で同じ症状の報告が複数上がったときに原因の切り分けが早くなります。
状況別の対処早見表
| 状況 | 対処 |
|---|---|
| 新規にSDKを導入する | 対処常に最新版を使う。このバグには最初から触れない |
| 既存プロジェクトでSDKバージョンを固定していない | 対処pip install --upgrade claude-agent-sdkで更新するだけで解決する |
| requirements.txt等でバージョンを固定している | 対処下限を>=0.1.40(クラッシュ回避のみ)または>=0.1.49(レート制限情報も使う)に緩めて再インストールする |
| CI/CDのDockerイメージが古いバージョンをキャッシュしている | 対処イメージの再ビルドが必要。requirements.txt側の下限も合わせて更新しないと同じ問題が再び起きます |
| 今すぐ更新できない事情がある | 対処issueのモンキーパッチで一時しのぎ(rate_limit_eventの中身は読めない) |
parse_message()を直接呼ぶ独自ラッパーがある | 対処更新後に戻り値がNoneになるケースへの対応を追加する |
まとめ
Python版Claude Agent SDKのMessageParseErrorは、CLIがrate_limit_eventのような未知のメッセージ型を送るたびにreceive_messages()の非同期ジェネレータを止めてしまうバグでした。原因はmessage_parser.pyのmatch文が未知の型を例外扱いしていたことで、SDK 0.1.40で「未知の型は黙ってスキップする」という前方互換な設計に変わっています。ただしrate_limit_eventの中身を実際に読み取れるようになったのは0.1.49で、RateLimitEvent型としての専用パースが入ってからです。クラッシュを止めるだけなら>=0.1.40、レート制限情報も使うなら>=0.1.49が下限の目安になります。更新できない場合の回避策はありますが、あくまで応急処置です。また、parse_message()を直接呼ぶ独自コードがある場合は、0.1.40以降で戻り値がMessage | Noneに変わっている点への追従も忘れないようにしてください。
CLI起動時のエラーやプロセス終了系のエラーなど、Agent SDKで遭遇するその他の一般的なエラーの切り分けはAgent SDKのエラー集にまとめています。ClaudeSDKClientとquery()のどちらでこの種のストリーミング処理を書くべきか迷う場合はquery() vs ClaudeSDKClient、Agent SDKの基本的なセットアップはClaude Agent SDK入門を参照してください。なおrate_limit_eventはSDK内部のメッセージパースの問題であり、API呼び出しそのものが429やoverloadedで拒否されるケースの対処はClaude rate limitエラーの対処が別の切り口になります。
関連する記事
Agent SDK をもっと見る →Claude Agent SDK入門 — Python / TypeScriptで最小エージェントを組む
「SDK execution error」でclaude-code-actionが落ちる原因と対処法
only prompt commands are supported in streaming modeエラーの原因
Agent SDKがDockerのDEBUG環境変数で動かなくなる原因と回避策
CLAUDE_ASYNC_AGENT_STALL_TIMEOUT_MSとは — サブエージェントのストール検知の設定
query() vs ClaudeSDKClient — Python Agent SDKの使い分け