Cloudflare Workers 無料プランの 3 MB 上限 — 何がバンドルに数えられ、何が数えられないのか
Cloudflare Workers の無料プランには、Worker のバンドルサイズに gzip 後 3 MB という上限があります。ドキュメントに書いてある数字ではあるのですが、実際に運用してみると「何がこの 3 MB に数えられて、何が数えられないのか」がわかっていないと判断を誤ります。
私は 8 サイトを Workers 上で運用していて、そのうち 1 サイトに動的 OG 画像生成を入れようとして、この上限に正面から当たりました。画像生成ライブラリを 1 つ足しただけで、余裕は一気に消えました。 一方で、同じサイトの静的アセットは合計 11 MB あるのに何の問題も起きていません。
この非対称性が上限の本質です。実測値とあわせて整理します。
上限の数字
Cloudflare のドキュメントに記載されている、Worker あたりのバンドルサイズ上限は以下のとおりです。
- Free プラン: gzip 後 3 MB
- Paid プラン: gzip 後 10 MB
注意したいのは、これが圧縮後の値だという点です。ビルド出力のファイルサイズをそのまま見て「まだ余裕がある」と判断しても意味がありません。逆に、非圧縮では数 MB あっても gzip がよく効く JavaScript なら通ります。
測り方
wrangler deploy に --dry-run を付けると、デプロイせずにサイズだけを出せます。
npx wrangler deploy --dry-run出力の最後のほうに、こういう行があります。
Total Upload: 9066.38 KiB / gzip: 2193.94 KiB見るべきは `gzip:` の後ろです。手前の Total Upload は非圧縮のサイズで、上限とは直接対応しません。この例では非圧縮 9 MB に対して圧縮後は 2.14 MB。JavaScript は 4 分の 1 近くまで縮むので、非圧縮の数字だけ見ていると必要以上に慌てることになります。
実測ベースライン
私が運用している Astro + EmDash 構成のサイトを 2 つ測ると、こうなりました。
- サイト A(この shinobiworks): 非圧縮 9066 KiB → gzip 後 2194 KiB。3 MB まで残り約 880 KiB
- サイト B(別サイト): 非圧縮 8654 KiB → gzip 後 2100 KiB。3 MB まで残り約 970 KiB
構成がほぼ同じなので当然ではありますが、どのサイトも 2.1 MB 前後に落ち着きます。つまり Free プランの 3 MB に対して、残っている余裕は 900 KB 程度しかない。
この「900 KB」という数字を持っているかどうかで、ライブラリを 1 つ足すときの判断がまるで変わります。
数えられないもの: 静的アセット
ここが一番の勘所です。`public/` に置いた静的アセットは、バンドルサイズ上限の対象外です。Cloudflare のドキュメントも、設定ファイルやバイナリデータはバンドルに含めず KV・R2・D1・Workers Static Assets に置くよう明示的に推奨しています。
実際、サイト A のビルド出力を見るとこうなっています。
$ du -sh dist/client
11M dist/client静的アセットだけで 11 MB あります。 3 MB の上限をとうに超えていますが、デプロイは通ります。--dry-run の出力にも、これらは別枠として現れます。
✨ Read 59 files from the assets directory .../dist/client
Total Upload: 9066.38 KiB / gzip: 2193.94 KiB「アセットディレクトリから 59 ファイル読んだ」という行と、Total Upload は別勘定です。画像やフォントをいくら足しても Worker のサイズは増えません。
この非対称性を知らないと、逆方向に間違えます。「画像を追加したらデプロイが落ちるかもしれない」と心配して画像を削る、といった無意味な最適化に走りがちですが、そこは削っても 1 バイトも効きません。
効くのは JavaScript と wasm
では何が 900 KB を食うのか。バンドルに取り込まれる JavaScript と WebAssembly です。とくに wasm は圧縮が効きにくく、そのままサイズに乗ってきます。
冒頭に書いた動的 OG 画像生成がまさにこれでした。記事タイトルを焼き込んだ OG 画像を実行時に生成するため、satori + resvg-wasm(workers-og)を 1 ルートぶんだけ追加しました。結果:
- 追加前: gzip 後 2.29 MB
- 追加後: gzip 後 2.97 MB
- 差分: 約 +680 KB
1 ルート追加しただけで、3 MB の天井に張り付きました。 内訳の大半は resvg の wasm です。動作自体は問題なく、生成時間も初回 109ms・2 回目以降は Cache API ヒットで 4ms と実用範囲でしたが、残り 30 KB では今後どんな依存も足せません。 この機能は見送りました。
「ライブラリを 1 つ足す」という日常的な判断が、Free プランではデプロイ不能に直結します。実行時に画像生成・PDF 生成・重い変換を行う系のライブラリは、入れる前に測るのが鉄則です。
逃がせるものは静的アセットへ
同じ OG 画像の実装で、フォントは意図的にバンドルから外しました。
日本語フォントは巨大です。Noto Sans CJK JP のフルセットは 1 ウェイト 16 MB あり、そのままではどこにも置けません。収録範囲を変えながら測るとこうなりました。
- かな・記号・約物のみ: 生 0.10 MB / gzip 0.07 MB
- cp932(JIS X 0208 + NEC/IBM 拡張、10,285 字): 生 1.65 MB / gzip 1.27 MB
- CJK 統合漢字 全域(U+4E00-9FFF): 生 5.03 MB / gzip 3.94 MB
- さらに拡張 A・互換漢字まで: 生 6.71 MB / gzip 5.32 MB
「いま公開している記事で実際に使われている文字だけ」に削る手もありますが、これは採れません。新しい記事で収録範囲外の漢字を 1 文字でも使った瞬間、その字が □ に化けて表示されます。 現実的な落とし所は cp932 で、日本語のブログタイトルとして書かれうる文字をほぼ網羅して 1.65 MB に収まります。
とはいえ、これをバンドルに入れると 2 ウェイトで gzip +2.5 MB。それだけで 3 MB の上限を超えます。 そこで public/fonts/ に置き、実行時に fetch してモジュールスコープに保持し、同じ isolate 内で使い回す形にしました。静的アセット側に置けば、フォントが 3.3 MB あってもバンドルサイズはゼロです。
判断の順序としてはこうなります。
- その依存は実行時に本当に必要か(ビルド時に済ませられないか)
- 必要なら、バイナリ部分を静的アセットに逃がせないか
- 逃がせないなら、測ってから入れる
もう一つの天井: 起動 1 秒
サイズだけが制約ではありません。ドキュメントには、Worker はグローバルスコープ(ハンドラの外側のトップレベルコード)を 1 秒以内にパース・実行し終える必要があると書かれています。そして「バンドルが大きいほど、またグローバルスコープでの初期化が重いほど起動時間は伸びる」と明記されています。
つまり 3 MB に収まっていても、トップレベルで重い初期化をしていれば別の理由で落ちます。大きな wasm をトップレベルでインスタンス化する実装は、サイズと起動時間の両方を同時に押し上げるので、とくに危険です。
いつ測るか
厄介なのは、サイズ超過が依存を足した PR の時点では表面化しないことです。
PR ごとに型チェックとテストは全サイトぶん自動で走りますが、これらはバンドルサイズを見ません。サイズが確定するのは実際に Workers へ上げる段です。私の構成では、そこまで踏む Preview デプロイを用意しているのは 8 サイトのうち 1 サイトだけです。個人で運用している小規模なサイト群なので、全サイトぶんのプレビュー環境は意図的に持っていません。
実害の範囲は限定的です。サイズ超過で失敗するのはデプロイ処理そのものなので、新しいバンドルが上がらないだけで、現行の Worker はそのまま動き続けます。 サイトが壊れるのではなく、更新が止まる。それでも気付くのが main への merge 後になるのは遅いので、重い依存を足す PR では手元で測る、というのを習慣にしました。
pnpm --filter <app> build
npx wrangler deploy --dry-run # gzip: の値を 3 MB と比べる余裕が 900 KB しかないとわかっていれば、この 2 コマンドを打つ動機が生まれます。逆に言えば、現在値を知らないことが最大のリスクです。
まとめ
- Free プランの Worker バンドル上限は gzip 後 3 MB(Paid は 10 MB)
- 測るのは
wrangler deploy --dry-runの `gzip:` の値。Total Upload(非圧縮)ではない - 静的アセットは上限の対象外。画像やフォントをいくら置いても Worker は太らない(実測で 11 MB のアセットを抱えたまま問題なく動いている)
- 太らせるのは JavaScript と wasm。画像生成ライブラリ 1 つで +680 KB、それだけで天井に届く
- バイナリは
public/に逃がして実行時fetchする。フォント 3.3 MB がバンドルサイズゼロになる - サイズとは別に起動 1 秒の制約がある。トップレベルの重い初期化は二重に効く
- 型チェックやテストではサイズは見えない。重い依存を足す PR では手元で測る(失敗してもサイトは止まらないが、気付くのは遅くなる)
無料プランで何ができるかという話は、Cloudflare Pages の無料プランは商用利用できるのかやVercel・Netlify・Cloudflare Pages の無料プラン比較にも書いています。無料プラン特有の制約という意味では、Cloudflare Pages 無料プランで next/image が使えない話も同じ系統の話です。