収集サーバの上り回線を使わない — エッジで画像を取得してストレージへ直保存する構成
この記事について
俺はZash。細い上り回線に張り付いた自宅サーバから、大量の画像を外部ストレージへ上げようとして、タイムアウトの壁にぶつかった話だ。
解決の骨子は一行で言える。画像の取得と保存を、回線の細いサーバから、太い回線を持つエッジ(Cloudflare Worker)へ丸ごと引っ越した。 住宅回線やモバイル回線のサーバでデータを扱っている個人開発者・小規模運用向けに、なぜその構成にしたのかと、途中でハマった落とし穴を書く。
試した構成: Cloudflare Workers + R2(オブジェクトストレージ)+ 収集サーバ(住宅/モバイル回線常駐)。
何が問題だったか — 細い上り回線で画像を運ぶと524で死ぬ
元の構成はシンプルだった。収集サーバが外部の画像URLを取得し、その画像データを自分でストレージやWordPressへアップロードする。ローカルで完結する素直な設計だ。
問題は回線だった。このサーバはデータセンターではなく、住宅回線やモバイルテザリングで動いている。上り帯域が細く、モバイル時は数十KB/sまで落ちる。そこへ数十枚の画像を通そうとすると、間に挟まっているトンネルの応答上限(約100秒)を超え、524タイムアウトや “context canceled” で保存そのものが失敗した。
切り分けは地道だった。トンネルのエラーログに「wp-post が途中で切れた」という記録が残っていて、そこで上り帯域が真犯人だと確定した。憶測で「サーバが重いのでは」と決めつけず、ログで応答が切れている場所を特定したのが効いた。
発想の転換 — 「サーバが運ぶ」をやめて「エッジが運ぶ」
Mix「画像って、そもそも収集サーバを一度も通らなくてよくないですか?」
Zash「そこだ。取得元もストレージも、両方クラウド側にある。細い回線のサーバをわざわざ経由させてるのが間違いだった」
画像の取得元(外部の画像ホスト)も、保存先(R2)も、どちらもクラウド上にある。なのに、両者の間をわざわざ住宅回線のサーバに中継させていた。ここが構造的な無駄だった。
そこで、Cloudflare Worker を中継役に立てた。
| 役割 | 旧構成 | 新構成 |
|---|---|---|
| 画像URLの取得元 | 外部画像ホスト | 外部画像ホスト(変わらず) |
| 誰が画像を取得するか | 収集サーバ(細い回線) | エッジのWorker(太い回線) |
| 誰がストレージへ書くか | 収集サーバ | Worker → R2 に直保存 |
| サーバが送るデータ | 画像本体(重い) | 画像URLのリストだけ(数百バイト) |
新しい流れはこうだ。収集サーバは「この画像URLをこのキーで保存して」というリスト(数百バイトのJSON)をWorkerへPOSTするだけ。画像の実データはサーバの上り回線を一切通らない。Workerがクラウドの太い回線で画像を取得し、R2へ書き込み、結果のURLを返す。
WordPressへの画像移植も同じ考え方で、Workerの別エンドポイント経由に寄せた。ストレージからWordPressメディアへ送る重い処理を、CloudflareとストレージとWordPressの太い回線だけで完結させ、サーバの上りを二度と通さないようにした。
ハマった落とし穴その1 — 403を「恒久失効」と誤判定して自己修復を塞ぐ
構成を変えたあとに残っていた地雷がこれだ。画像ホストが一時的に返す 403(レート制限やクラウドIPへのプチBAN)を、コードが**「その画像は恒久的に消えた(expired)」と誤判定していた**。
一度expired扱いになると、再取得のロジックがURL重複としてスキップしてしまう。つまり、生きている画像が一過性の403一発で永久に欠落する。リトライもフォールバックもされない。
修正の方針はこうした。
- 恒久失効とみなすのは
404 / 410だけにする。403や、画像の代わりに返ってくるHTMLは「一時的なエラー(リトライ対象)」に分類する。 - エッジのWorkerがエラーを返した画像は、サーバ側の回線でフォールバック取得してから諦める。
- 再取得ロジックを「成功していない画像を拾い直す」形に変え、一時失敗が次の巡回で自己回復するようにした。
教訓は普遍的だ。外形的なエラーコードの分類を雑にすると、リトライ可能な失敗をリトライ不能に落として自ら傷を深くする。 一時的な失敗と恒久的な失敗を混ぜてはいけない。
ハマった落とし穴その2 — 素のHTTPクライアントがエッジへ届かず、画像が黙って裏道に落ちていた
もう一つ、もっと厄介な「無言の死」があった。サーバのランタイムに入っていた素のHTTPクライアントが、WorkerへのHTTPS接続で証明書検証にことごとく失敗していたのだ。
その結果、Workerを呼び出す関数が常に失敗を返し、画像オフロードが無言で機能停止していた。表向きエラーは出ない。だが実態は、逃がしたはずの画像が全部細い回線のサーバ側フォールバックに落ちていた。設計と真逆の挙動が、誰にも気づかれずに続いていた。
HTTPクライアントをブラウザ相当の実装に差し替えたら、証明書検証もエッジ側のbot判定も両方すり抜けて、正常に200が返るようになった。
この手の「設定は正しいのに、片方の経路が黙って死んでいて、こっそり別経路に落ちている」障害は、成功しているように見えて一番タチが悪い。「オフロードできているはず」を鵜呑みにせず、実際にどの経路を通ったかを一度は実測で確認するべきだった。
やってみてわかったこと
回線が制約になる運用では、「どのマシンが処理するか」より「どのデータが、どの回線を通るか」で設計を見たほうがいい。今回で言えば、重い画像本体を細い回線から追い出し、太い回線を持つエッジに全部押し付けたのが本質だった。サーバが運ぶのはURLのリストという軽い情報だけでいい。
そして、この手のオフロードは「入れて終わり」ではない。403の分類ミスも、証明書検証の失敗も、どちらも**「逃がしたはずのものが、こっそり元の重い経路に戻っていた」**という形で牙をむいた。憶測を1つ挟むたびに1回外した。最後に効いたのは、コミット前に実機で1回叩いて経路を確認する、という一番泥臭い手順だった。
更新履歴
- 2026-07-06: 初稿