42
2026.09 / Cloudflare Workers

Workers のコールドスタート戦略 — 2020 年の理解をアップデート

Workers のバンドルサイズ上限の変更は、64 MiB への引き上げではなく、圧縮後のサイズ検査の撤廃でした。その背景には、ハンドシェイク中の先読みから Shard and Conquer へ、コールドスタート戦略の転換があります。

2026 年 9 月 4 日に、Workers のバンドルサイズの上限が変更されました。CHANGELOG を見て、すぐこうポストしました。

事実としては間違っていませんが、この書き方だと「10 MB だった上限が 64 MB に引き上げられた」と読めてしまいます。いいねとリポストの伸び方を見ると、そう受け取った人も多そうです。実際には、上限は上がっていません。変わったのは検査の仕方で、その中身をまず正確に書いておきます。

もう 1 つ、この機会に調べ直したことがあります。バンドルサイズの上限を上げられない理由として以前聞いていたのが、コールドスタートでした。それが今どうなっているのかを追ったところ、自分の理解が 2020 年で止まっていたと分かりました。後半はその話です。

撤廃されたのは圧縮後のサイズ検査

CHANGELOG にはこう書かれています。

Previously, Cloudflare checked that compressed size and rejected deploys over 3 MB (Free) or 10 MB (Paid). That limit has been removed. Cloudflare now only checks the uncompressed size of your bundle, which is 64 MiB across all plans.

以前は圧縮後のサイズを検査し、3 MB (Free) または 10 MB (Paid) を超えるデプロイを拒否していました。この制限は撤廃されました。現在 Cloudflare が検査するのは非圧縮のバンドルサイズだけで、全プラン共通の 64 MiB です。 — Deploy larger Workers — up to 64 MiB for both free and paid plans, Cloudflare Changelog, 2026-09-04

やめたのは圧縮後のサイズの検査であって、上限を引き上げたとは書かれていません。では非圧縮の 64 MiB はいつからあったのか。cloudflare-docs リポジトリで limits ページの履歴をたどると、今回のコミットで消えた行が分かります。

| After compression (gzip) | 3 MB | 10 MB |
| Before compression | 64 MB | 64 MB |
| Worker size (uncompressed) | 64 MiB | 64 MiB |

削られたのは gzip の行だけで、64 MB の行はそれ以前からあります。さかのぼれた範囲では、2025 年 12 月時点のページにも「On either plan, a Worker can be up to 64 MB before compression」(どちらのプランでも、Worker は圧縮前で 64 MB まで) と書かれていました。

wrangler 側も同じ日に更新されています。デプロイのたびにバンドルサイズを警告する処理の比較対象が、gzip 後のサイズと 3 MiB から、非圧縮サイズと 64 MiB に変わりました。

// Current max is 3 MiB for free accounts, 10 MiB for paid accounts.
const MAX_GZIP_SIZE_BYTES = 3 * ONE_KIB_BYTES * ONE_KIB_BYTES;
// Max uncompressed Worker size is 64 MiB for all accounts.
const MAX_UNCOMPRESSED_SIZE_BYTES = 64 * ONE_KIB_BYTES * ONE_KIB_BYTES;
const percentage = (gzipSize / MAX_GZIP_SIZE_BYTES) * 100;
const percentage = (size / MAX_UNCOMPRESSED_SIZE_BYTES) * 100;

つまり、非圧縮で 64 MB という上限は以前からあり、その手前に圧縮後で 10 MB (Free は 3 MB) という検査が重なっていました。今回なくなったのは、この手前の検査です。

この 2 段の検査が、バンドルの中身によってどう効いていたかを整理します。JS は gzip でおよそ 3 分の 1 に縮むので、圧縮後 10 MB の検査は非圧縮 30 MB 前後まで通していました。64 MB には届きませんが、余裕はあります。一方、Wasm や埋め込んだバイナリ、すでに圧縮されているデータは gzip でほとんど縮みません。これらを多く含むバンドルは、非圧縮の 64 MB よりずっと手前の 10 MB あたりで、圧縮後の検査に引っかかっていました。部屋がどれだけ広くても、入り口が狭ければ大型の家具は運び込めない、というのに近い状況です。今回取り払われたのは、その入り口のほうでした。

gzip で約 1/3 に縮む
minified な JS

圧縮後 10 MB の検査は、非圧縮 30 MB 前後まで通していました。64 MiB との差は 2 倍ほどで、今回広がるのはこの分です。

gzip でほとんど縮まない
Wasm・バイナリ・圧縮済みデータ

圧縮後も非圧縮もほぼ同じサイズなので、実質 10 MB で止まっていました。64 MiB まで一気に伸びる、今回いちばん影響が大きいところです。

検査の値そのものが小さかった
Free プラン

圧縮後 3 MB は非圧縮で 10 MB 弱にあたります。Paid との差もなくなり、64 MiB まで使えます。

バンドルサイズとコールドスタートのジレンマ

数年前、Cloudflare の中の人からバンドルサイズの上限はそう簡単には上げられないと聞いたことがあります。理由はコールドスタートでした。スクリプトが大きくなるとロードに時間がかかり、それが最初のリクエストの遅延として出てしまう。だから上限は慎重に決めている、という説明です。

このとき前提になっていた仕組みは、2020 年のブログ「Eliminating cold starts with Cloudflare Workers」で説明されています。TLS ハンドシェイクの最初のパケットである ClientHello には SNI が載っていて、そこからホスト名が分かります。ハンドシェイクが終わるまでの間に Worker のロードを済ませてしまう、という仕組みです。

クライアントCloudflareClientHello (SNI にホスト名が載る)Worker をロード5 ms。ハンドシェイクの間に終わるServerHello / 証明書鍵交換の完了ハンドシェイクHTTP リクエスト起動済みの Worker がすぐ処理するロードは往復の待ち時間に隠れるので、見える遅延はゼロ
FIGTLS ハンドシェイク中の先読み。ClientHello のホスト名で Worker のロードを始め、リクエストが届く前に終わらせる。

Since it only takes 5 milliseconds to load a Worker, and the average latency between a client and Cloudflare is more than that, the cold start is zero.

Worker のロードにかかるのは 5 ミリ秒だけで、クライアントと Cloudflare の間の平均レイテンシはそれより長い。だからコールドスタートはゼロになる。 — Eliminating cold starts with Cloudflare Workers, Cloudflare Blog, 2020-07-30

ロードに 5 ミリ秒、クライアントと Cloudflare の間の平均レイテンシはそれより長い。だから待ち時間に隠れて見えなくなる。この仕組みは知っているつもりでいました。

知識が止まっていたのは、この先でした。

コールドスタートがハンドシェイクより長くなった

この方法が成り立つのは、TLS ハンドシェイクのほうがコールドスタートより長いあいだだけです。2025 年 9 月のブログ「Eliminating cold starts 2: shard and conquer」では、その前提が崩れたことが認められています。

Taken together, cold starts for complex applications began to lose the TLS handshake race.

これらが重なって、複雑なアプリケーションのコールドスタートは TLS ハンドシェイクとの競走に負けはじめた。 — Eliminating cold starts 2: shard and conquer, Cloudflare Blog, 2025-09-26

理由として挙げられているのは、2020 年以降に少しずつ緩められてきた 2 つの上限です。

上限 2020 年 2025 年 9 月
Worker サイズ (圧縮後・Paid) 1 MB 5 MB を経て 10 MB
Worker サイズ (圧縮後・Free) 1 MB 3 MB
起動 CPU 時間 200 ms 400 ms

スクリプトが大きくなれば、ストレージからランタイムへの転送もコンパイルもその分だけ重くなります。起動 CPU の枠が広がれば、top-level の実行に使える時間も長くなります。一方でハンドシェイクは短くなりました。TLS 1.2 が 3 往復だったのに対し、TLS 1.3 は 1 往復で終わります。隠したい時間が延びる一方で、隠すための時間は短くなっていたわけです。

クライアントCloudflareClientHelloWorker をロードスクリプトは 10 倍、起動 CPU は 2 倍にServerHello 〜 Finished (TLS 1.3 は 1 往復)ハンドシェイクHTTP リクエストロードが終わるまで待つ見える遅延レスポンスロードがハンドシェイクより長くなり、はみ出した分がコールドスタートとして見える
FIG上限が緩んだあとの同じ流れ。ロードがハンドシェイクより長くなり、はみ出した分が遅延として見える。

そこで方針が変わります。コールドスタートの時間を隠すのをやめ、回数そのものを減らす方向に切り替えられました。

リクエストが分散するとコールドスタートは増える

以前は、データセンター内のどのサーバがリクエストを受け取っても、そのサーバで isolate が起動していました。同じ Worker が隣のサーバですでに動いていても、そちらへ回さず、リクエストを受け取ったサーバ自身で起動します。

ブログに出ている例をそのまま借ります。300 台のサーバがあるデータセンターに毎分 1 件のリクエストが均等に分かれると、1 台あたりに届くのは 5 時間に 1 回です。混んでいるデータセンターではその間に evict されてしまうので、次のリクエストはまたコールドスタートになります。トラフィックの少ない Worker では、コールドスタート率が 100% になることもあり得た、と説明されています。プロトタイプ段階のユーザーに「トラフィックが増えればレイテンシは下がります」と説明する羽目になっていた、という話も出てきます。

同じリクエストを 1 台に集めれば、そのサーバには毎分届くことになるので evict されません。メモリの使用量も 299/300、つまり 99.7% 減ります。空いた分だけ、ほかの Worker も残りやすくなります。

以前Worker sharding 以後着いたサーバでそれぞれ起動する1 件目2 件目3 件目サーバ 1サーバ 2サーバ 3evictevictevictコピーが台数ぶん生まれ、大半は 1 回で捨てられるリングを引いて 1 台へ転送する1 件目2 件目3 件目shard serverisolate は 1 つ起動したまま残るリクエストが続くので evict されないコピーは 1 つ。メモリは 299/300 減る
FIG同じ Worker 宛のリクエストの行き先。散らばると毎回コールドスタートになり、集約すると isolate が残る。

集約先の決め方には、CDN の HTTP キャッシュで昔から使われている consistent hash ring がそのまま流用されています。ハッシュ値の取りうる範囲を 0 から最大値まで一本の数直線に並べ、端と端をつないで輪にしたものです。スクリプト ID とサーバのアドレスをそれぞれハッシュして、この輪の上に置きます。スクリプトの位置から値が大きくなる方向へ進み、最初に見つかったサーバがその Worker の担当です。この方式が使われているのは、サーバが増減しても輪の一部の担当が動くだけで済むからです。単純なハッシュテーブルでは、1 台入れ替わるだけで全 Worker の担当が変わり、そのたびに全部がコールドスタートからやり直しになります。

割り当てサーバ C が抜けたとき0 と最大値がつながるサーバ Aサーバ Bサーバ Cサーバ DWorker X のハッシュ値スクリプトの位置から値が大きくなる方向へ進む最初に見つかったサーバ C が担当0 と最大値がつながるサーバ Aサーバ Bサーバ C が抜けるサーバ DWorker X のハッシュ値C が抜けてもA・B の担当は変わらない担当が D に移るのは B〜C の区間の Worker だけ
FIGconsistent hash ring。スクリプトとサーバを同じ輪の上に置き、スクリプトの位置から値が大きくなる方向にある最初のサーバが担当になる。1 台抜けても、動くのはその手前の区間だけ。

リクエストを最初に受け取ったサーバは shard client として動き、リングを引いて shard server へ転送します。効果は数字で示されています。Enterprise のトラフィックのうち実際にシャードされたのは 4% だけで、残りの 96% はもともと 1 台では捌けない Worker でした。その 4% を集約しただけで、全体の eviction 率は 1/10 に、warm request 率は 99.9% から 99.99% に上がっています。リクエストの分布がべき乗則に従うため、ロングテール側を少し整理するだけで全体が大きく動く、という説明です。

集約してもコストは増えない

ここで気になったのが費用です。Workers の課金は CPU 時間に対してだけで、isolate が起動したまま待っている時間には発生しません。1 台に集めて起動したままにしておくなら、その分のコストは Cloudflare 側がかぶっていることになります。

実際には逆でした。シャーディングの目的は起動したままにしておくことではなく、無駄なコピーを減らすことにあります。以前は同じ Worker の isolate がデータセンター内に何十個も作られ、その大半が一度使われただけで捨てられていました。1 台にまとめれば、同じリクエスト数でも isolate は 1 つで済みます。メモリの使用量は減り、リクエストの間隔が詰まる分だけ evict もされにくくなります。リクエストが集まった結果として残っているだけなので、トラフィックが途切れれば、これまでどおり解放されます。何秒アイドルが続くと解放されるのかといった具体的な値は、公開されていません。

コストの話も同じところに行き着きます。isolate は起動時のメモリが Node のプロセスより 1 桁小さく、待機中は CPU をほとんど使いません。むしろ以前の重複起動のほうが、コールドスタートのたびに parse と compile の CPU を消費し、コピーの数だけメモリも占めていました。ブログでは eviction 率がシステム全体のメモリ圧の指標だと説明されていて、それが 1/10 になったこと自体が負荷の低下を表しています。

shard server に負荷が集まりすぎて捌けないときは、リクエストを最初に受け取ったサーバ (shard client) が自分で Worker を起動して処理します。ここで問題になるのが、shard server が処理できないことをどう伝えるかです。先に処理の可否を問い合わせてから本体を送る方式だと、往復が 1 回増えてしまいます。

Cloudflare の設計では、shard client はリクエスト本体を先に送ってしまいます。そのとき、自分のサーバにある「まだ起動していない Worker」を指す参照 (Cap’n Proto の capability) を一緒に付けておきます。shard server が処理できない場合はエラーを返す代わりに、この参照をそのまま返します。shard client 側の RPC 層は、返ってきた参照が自分のものだと分かるので、送りかけていたデータを止め、自分のサーバでの処理に切り替えます。問い合わせの往復を増やさずに済む仕組みです。

先に問い合わせる方式shard clientshard server処理できるか?OKリクエスト本体レスポンス可否を確認する往復が 1 回増えるCloudflare の方式shard clientshard serverリクエスト本体と自分の未起動 Worker への参照処理できない参照をそのまま返す自分の参照だと分かる送信を止め、自分で Worker を起動して処理処理完了問い合わせの往復なし。次からは転送せず自分で処理
FIGshard server が処理できないときの切り替え。先に問い合わせると往復が増えるので、本体と一緒に自分の未起動 Worker への参照を送り、断るときはその参照を返してもらう。

一度こうして断られた shard client は、その Worker を自分のサーバで起動済みになります。次からは転送せず、自分で処理します。

起動時間 1 秒の制限は残っている

では今回の変更でトレードオフがなくなったかというと、そうではありません。limits には今も「Larger Worker bundles can impact startup time」(大きな Worker バンドルは起動時間に影響しうる) と書かれています。

制限 値 今回
Worker サイズ (圧縮後) 3 MB / 10 MB 撤廃
Worker サイズ (非圧縮) 64 MiB 据え置き。サイズの上限はこれだけになった
Worker startup time 1 秒 据え置き
Memory per isolate 128 MB 据え置き

この 1 秒は、実行時ではなくデプロイ時に検査されます。global scope の parse と実行が 1 秒に収まらない場合、デプロイの検証で Script startup exceeded CPU time limit (エラーコード 10021) として弾かれます。起動の重い Worker が、重いまま本番で動きつづけることにはなりません。弾かれたときは wrangler が CPU プロファイルを出力します。top-level のどこに時間がかかっているかは、Chrome DevTools か VS Code で確認できます。

デプロイ時実行時バンドルgzip 後 3 / 10 MB2026-09-04 に撤廃この検査だけが撤廃された非圧縮 64 MiB前からある上限起動 1 秒global scope の parse と実行完了10021 で弾かれるリクエストisolate 128 MBJS heap と Wasm超えると 1102 / exceededMemoryin-flight は完了させ、新しい isolate を作る
FIG残っているゲート。サイズの検査は 1 つ減り、起動時間とメモリはそのまま。

つまり、コールドスタートを抑えているのはサイズの上限ではなく起動時間の上限です。圧縮後の検査がなくなったあとも、こちらはそのまま残っています。

では、なぜこのタイミングで圧縮後の検査を外したのか。公開情報からは分かりませんでした。分かっているのは、5 月には準備ができていたことだけです。docs の CHANGELOG ファイルは当初 2026-05-22 の日付で用意され、公開時に 9 月 4 日へ書き換えられています。wrangler 側の変更も、同じ 9 月 4 日にマージされました。

最初は、AI エージェント向けの機能を広げるために緩めざるを得なかったのかと考えました。ただ、Python Workers を見ると、Pyodide とパッケージの初期化はデプロイ時に済ませ、Wasm のメモリをスナップショットとして配る設計になっています。バンドルサイズの上限にぶつからない作りなので、動機としては弱いと思っています。

もう 1 つの読み方は、単に冗長な検査を片付けただけ、というものです。非圧縮 64 MB の上限がある以上、圧縮後 10 MB の検査は二重のゲートでした。Shard and Conquer でコールドスタートの回数を抑えられるようになり、大きなバンドルを許しても影響が小さくなった。それで片方を外した、というのが今のところの推測です。

デプロイ前に確認すること

サイズを確認する方法は CHANGELOG に載っています。

Terminal window
wrangler deploy --outdir bundled/ --dry-run
Total Upload: 259.61 KiB / gzip: 47.23 KiB

64 MiB と比べられるのは Total Upload の値です。gzip の値は参考として表示されるだけです。起動時間のほうは、wrangler deploy か wrangler versions upload の出力に startup_time_ms として出ます。

一次情報

Cloudflare の公式ドキュメント・CHANGELOG・ブログと、公開リポジトリのコミットのみを参照しています。調査日は 2026 年 9 月 5 日です。gzip 比だけは自分で実測した値です。

変更そのもの Deploy larger Workers — up to 64 MiB for both free and paid plans (2026-09-04) / Workers Limits / cloudflare-docs #33241 / workers-sdk #14001

コールドスタート Eliminating cold starts with Cloudflare Workers (2020-07-30) / Eliminating cold starts 2: shard and conquer (2025-09-26) / How Workers works / How Python Workers Work

公開 2026年9月5日