前提: id のポーリングは「確実に動く手段」だったはず
以前、XserverへのSSHをやめてCloudflare Workersへ移行したで書いたとおり、デプロイ完了の検知には wrangler deployments status --json が返す deployment id を使っていました。
Deploy Hook を叩く前に id を記録し(baseline)、叩いた後に同じコマンドをポーリングして id が変わるのを待つ。レスポンス形状の勘違いや権限テンプレートの罠を踏んだ末にたどり着いた、実測で確認済みの方法でした。当時はこれを「確実に動く手段」と書きました。
id が変わったら、そのままX告知を投稿する処理を組みました。 検知できたのだから公開は終わっている、という前提です。
症状: url_status: 404 を出力しながら投稿していた
ある日の自動投稿でOGPカードが表示されませんでした。実行ログを見ると、原因は一目瞭然でした。
url_status: 404
⚠ URL状態が200ではありません(status=404)。CDN/Cloudflareのbot遮断の可能性もあるため、
このまま投稿を続行します。
[OK] 投稿しました tweet_id=...
404だと分かっていながら投稿していました。 手動実行(事前にページを目で確認してから流した回)では url_status: 200 になっていたので、自動実行だけがこの状態でした。
真因: id の変化は「アクティブになった」だけだった
実行時刻を並べると、原因は3秒に収まっていました。
| 時刻(UTC) | 出来事 |
|---|---|
| 22:02:00 | 新しい deployment id を検知 |
| 22:02:03 | X へ投稿 |
deployment id が変わることと、そのデプロイがエッジまで反映されていることは別でした。id はコントロールプレーン側の状態で、変わった瞬間には世界中のエッジには届いていません。3秒という短い間隔がそれを可視化しました。
もし反映に数秒かかっただけなら、次に投稿を試したときには直っていたはずです。ところが直りませんでした。
なぜ後から200になっても直らないのか
Xはカードを投稿された瞬間にクロールします。そのとき404を受け取ると、その結果をキャッシュします。ページが数秒後に200になっても、Xは再クロールしに来ません。カードなしの投稿は、その投稿である限り直らない状態で固定されました。
もう1つ、これを見逃した理由があります。404を警告として扱っていたことです。
if status != 200:
print(f" ⚠ URL状態が200ではありません(status={status})。CDN/Cloudflareの"
f"bot遮断の可能性もあるため、このまま投稿を続行します。")
このガードは元々、別の問題への対処でした。CloudflareがGitHub Actions runnerのIPをbot遮断して403を返すことがあり、runnerから見た非200が必ずしも「未公開」を意味しないため、ハード失敗にしていませんでした。
404と403を同じ「非200」として一括りにしていたのが穴でした。 403はbot遮断の疑いで警告に留めるべきですが、404は「そのページがまだ存在しない」という別種の情報です。区別せずに投稿を続けたので、本当に未公開のときも通り抜けていました。
是正: 実URLが200になるまで待ち、404は投稿を止める
2点直しました。
1. 実URLのポーリングを追加する。 deployment idではなく、記事の実URLそのものが200を返すまで待つようにしました。
def wait_until_published(url, attempts=10, interval=15):
for i in range(1, attempts + 1):
status = check_url_status(url)
if status == 200:
return status
print(f" 公開待ち {i}/{attempts}: status={status}")
if i < attempts:
time.sleep(interval)
return status
id の変化を「たぶん終わっている」の代理指標として使うのをやめ、確認したいことをそのまま確認する形にしました。回数と間隔は環境変数で調整できるようにしてあります。
2. 404/410はハード失敗にする。 403だけを警告のまま残しました。
if status in (404, 410):
sys.exit(f"[FATAL] 記事URLが未公開です(status={status}): {url}")
if status != 200:
print(f" ⚠ URL状態が200ではありません(status={status})。"
f"CDN/Cloudflareのbot遮断の可能性もあるため、このまま投稿を続行します。")
「非200」は1種類ではありませんでした。 「まだ存在しない」と「存在するが見えないふりをされている」を同じ扱いにしていたことが、そもそもの設計ミスでした。
ワークフロー側にも一呼吸置きました。id を検知した直後に20秒待ってから次の処理へ進みます。実URLのポーリングと合わせて二重の安全弁にしています。
テストで一度、空振りした
是正した後、これで直ったことを確認しようとして、最初に用意した検証は空振りでした。既に投稿済みの記事のURLで試したので、投稿処理そのものに到達する前に「投稿済みなのでスキップ」という別の分岐に入り、404のハード失敗を一度も通っていませんでした。
未投稿の記事のURLに変えてやり直すと、狙いどおり「公開待ち」のログを出しながら404を検知し、投稿せずに止まりました。「テストが通った」を見た瞬間、それが本当に狙った経路を通ったのかを確認する必要があります。 通った理由が別の分岐だったら、直したはずの穴はまだそこにあります。
教訓: 検知に使う指標と、確認したい状態を混同しない
deployment idのポーリングは、それ自体は正しく動いていました。「デプロイが受理されたか」を知るには十分な指標でした。
問題は、知りたかったことが違った点です。知りたかったのは「利用者から見てそのページが200を返すか」で、id はその代理にすぎませんでした。代理指標と実指標の間には、今回のように数秒でも埋まらない溝ができることがあります。
「確実に動く手段」という評価も、それ自体は間違っていませんでした。id のポーリングは確実に id の変化を検知します。確実だったのは検知であって、検知した先に起きていることの保証ではありませんでした。
追記(2026-08-17): 代理指標そのものを撤去しました
この記事を書いたあと、id のポーリングは二重の安全弁として残していました。それを撤去しました。きっかけは別の障害です。
2026-08-14 から3日間、この自動告知が毎日失敗していました。エラーはこれです。
A request to the Cloudflare API
(/accounts/<account_id>/workers/scripts/<script>/deployments) failed.
Authentication error [code: 10000]
wrangler がJSONを返さないので、その出力を受け取る後続処理が例外で落ち、X告知もIndexNow通知も丸ごと止まっていました。サイトの配信もビルドも正常で、止まっていたのは告知だけです。
切り分けると、トークンは失効していませんでした。
| 呼び出し | 結果 |
|---|---|
GET /user/tokens/verify | 200 / status: active |
GET /accounts | 200 / 0件 |
GET /accounts/{id} | 403 / 9109 Unauthorized to access requested resource |
トークンは有効なまま、到達できるリソースがゼロになっていました。 失効なら分かりやすいのですが、active のまま権限だけが消えると「認証は通っているのに何も読めない」という状態になります。
ここで、この記事の結論をそのまま適用しました。id のポーリングは代理指標です。本当に確認したいのは実URLの200で、それは既に wait_until_published() が見ています。代理指標を残すために資格情報を1つ持ち続け、その資格情報が腐ると告知が丸ごと止まる——確認したい状態を直接見られるなら、代理指標は残す価値より壊れる余地の方が大きいという判断です。
やったことは3つです。
wrangler deployments statusのポーリングを削除し、Deploy Hook を叩いたら直ちに告知処理へ進む- 実URLの待ち時間を150秒から10分へ延長する(ビルド所要は実測76秒。従来はポーリングがこの時間を吸収していました)
- Deploy Hook のレスポンスが
"success":trueでなければ即座に失敗させる(起動できていないなら、いくら待っても公開されないため)
これで、この経路からCloudflareのAPIトークンが不要になりました。
失ったものもあります。**新記事が無い日はビルド失敗を検知できなくなりました。**告知処理は「本日公開の記事なし」で即終了し、URLを待たないためです。ここは告知の正しさとは別の関心事(外形監視の領分)だと整理しました。
まとめ
- deployment idの変化は「デプロイが受理された」ことしか示しません。エッジへの反映完了は別の事実で、数秒のずれが実害になります
- Xはカードを投稿時点でクロールし、404を受け取るとキャッシュします。後からページが直っても、その投稿のカードは直りません
- 「非200」を一括りにしないでください。403(bot遮断の疑い)と404(未公開)は別の意味を持ちます
- 代理指標(id の変化)で確認した気になっていないか、本当に確認したい状態(実URLの200)を直接見ているかを分けて考えてください
- 修正の検証では、テストが通った理由が狙った経路かどうかを確認してください。私は一度、別の分岐で空振りしました
- (追記)実指標を直接見られるようになったら、代理指標は残さず消す方が安全です。残せば資格情報を持ち続けることになり、その資格情報が腐ったときに機能ごと止まります

