Cloudflare Workers の Limits にふれる - メモリー編
ご挨拶
Japanese Traditional Company でアプリケーションエンジニア、趣味でもアプリケーションエンジニアをしている Yorsh と申します。
個人サイト: Studio of Yorshもやっております、よかったら覗いてみてください。
概要
今回は Cloudflare Workers のメモリ上限について、これを意識しない(知らない)とどうなるかを確認し、上限値への対応を紹介します。
実際には、Cloudflare workers 上に、hono を用いて、R2 オブジェクトストレージからファイルをダウンロードできる API[1]を構築します。
このとき、知らない人の実装とそれを直す方法を並べます。
バッドプラクティス: なにも知らない人がファイルダウンロード API を作ってみる
本投稿で扱うソースコードはGitHub - yorsh1111/SAMPLE-memory-limits-of-cloudflare-workersにあります。
セットアップ
Cloudflare Workers - Hono を参考にセットアップ[2]します。
実装
SAMPLE-memory-limits-of-cloudflare-workers/src/index.ts at main · yorsh1111/SAMPLE-memory-limits-of-cloudflare-workersのように実装します。
以下がポイントです
app.get("/files/:id", async (c) => {
// 中略
// R2からファイルを取得
const object = await bucket.get(fileId);
// 中略
// ArrayBufferとして展開
const arrayBuffer = await object.arrayBuffer();
// 中略
});
デプロイ
$ npm run deploy
ここではhttps://example.comにデプロイしたものとします。
適当なファイルを R2 に配置する
以下のファイルを、Cloudflare Dashboard から R2 にアップロードします。
$ du -h test1.wav
21M test1.wav
$ du -h test2.wav
144M test2.wav
バッドプラクティスの API を試す
ファイルの存在を確認する
$ curl https://example.com/files | jq '.'
{
"success": true,
"count": 2,
"files": [
{
"id": "test1.wav",
"name": "test1.wav",
"size": 21740396,
"uploaded": "2025-10-30T21:14:29.473Z"
},
{
"id": "test2.wav",
"name": "test2.wav",
"size": 141555302,
"uploaded": "2025-10-30T21:15:02.543Z"
}
]
}
test1.wav と test2.wav というファイルがあり、正常にアップロードできていることが確認できます。
余談ですが、jqを使うと返却値の JSON を pretty にしてくれるので、楽です。
ファイルをダウンロードし、中身を確認する
$ curl -o download-test1.wav https://example.com/files/test1.wav
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 20.7M 100 20.7M 0 0 8798k 0 0:00:02 0:00:02 --:--:-- 8802k
$ curl -o download-test2.wav https://example.com/files/test2.wav
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 152 100 152 0 0 1168 0 --:--:-- --:--:-- --:--:-- 1178
見た目上はダウンロードできていそうです。
まずはファイルサイズを確認しましょう。
元々は以下の状態でしたので、
$ du -h test1.wav
21M test1.wav
$ du -h test2.wav
144M test2.wav
ダウンロードしたファイルでもサイズが変わっていないことを期待します、が。
$ du -h download-test1.wav
21M download-test1.wav
$ du -h download-test2.wav
4.0K download-test2.wav
download-test2.wavのファイルサイズが変わってしまっています。
中身を確認してみましょう。
$ cat download-test2.wav | jq .
{
"success": false,
"error": "ファイルのダウンロード中にエラーが発生しました",
"details": "Memory limit would be exceeded before EOF."
}
Wow! memory の limit に到達したようです。
何がいけなかったのか
先に意識していただいた下記が問題です。
app.get("/files/:id", async (c) => {
// 中略
// R2からファイルを取得
const object = await bucket.get(fileId);
// 中略
// ArrayBufferとして展開
const arrayBuffer = await object.arrayBuffer();
// 中略
});
ここでオンメモリにファイルの ArrayBuffer を展開しています。
よって、21M の test1.wav では問題になりませんでしたが、144M の test2.wav では問題になったということです。
ベタープラクティス: 直してみる
先ほどの実装では、object.arrayBuffer()を使用して R2 からのファイルをメモリ上に全て読み込んでいました。
これがメモリ制限の問題を引き起こしていました。
Cloudflare Workers の R2 オブジェクトストレージは、arrayBuffer()の他にbodyというプロパティを提供しています。
これはストリームとしてデータを取得できるReadableStreamオブジェクトです。
修正コードは以下のようになります。
app.get("/files/:id", async (c) => {
// 中略
// R2からファイルを取得
const object = await bucket.get(fileId);
// 中略
// ストリームとして取得(メモリに全て展開しない)
const stream = object.body;
// レスポンスヘッダーを設定
const headers = new Headers();
headers.set(
"Content-Type",
object.httpMetadata?.contentType || "application/octet-stream"
);
headers.set("Content-Disposition", `attachment; filename="${fileId}"`);
// ストリームを使用するため、Content-Lengthはオブジェクトのサイズから設定
if (object.size) {
headers.set("Content-Length", String(object.size));
}
// ストリームを直接Responseに渡す
return new Response(stream, {
status: 200,
headers,
});
// 中略
});
この修正により、ファイル全体をメモリに読み込まず、ストリームとして少しずつクライアントに送信できるようになります。
これによりメモリ使用量が削減され、Cloudflare Workers のメモリ制限(128MB)を超えることなく大きなファイルも処理できるようになります。
これでデプロイして確認してみましょう。
修正版 API でファイルをダウンロードしてみる
$ curl -o download2-test2.wav https://example.com/files/test2.wav
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 134M 100 134M 0 0 8337k 0 0:00:16 0:00:16 --:--:-- 10.7M
$ du -h download2-test2.wav
144M download2-test2.wav
オリジナルのファイルと、ダウンロードしたファイルのサイズが一致していることが確認できました。さらに内容も比較してみます。
$ if cmp -s test2.wav download2-test2.wav; then
echo "一致しています"
else
echo "一致していません"
fi
一致しています
このように、正しくダウンロードが出来るようになりました。
総評と一般論
Cloudflare Workers では、メモリ使用量に 128MB という明確な制限があります。
今回のようなファイル操作では簡単に制限を超えてしまう可能性があります。
今回は、ストリームを用いて制限を回避してみました。
そもそも「できるだけメモリに展開しない」という考え方は、重要です。
「ちっ、Cloudflare Workers は 128MB なのかよ、めんどくせーな」ではなく、どんな PF でもリソース制限を正しく理解し、効率的なコードを書くよう心がけましょう。
また、どうしても 128MB を超えるメモリ展開がある場合には、別 PF の使用を検討しましょう。
以下、おまけです。
おまけ
ローカルでテストしていれば気づくのでは?
「いや、ローカルのテストで気づかないのかよ?お前は wrangler dev しないのか?」
やってみましょう。
まずは r2 にファイルを配置してみます。
test2 について分かればいいので、それだけ配置します。
$ wrangler r2 object put --local 20251031-test/test2 --file ./test2.wav
変更前のブランチに切り替えます。
$ git checkout main
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
開発サーバを立ち上げ、リクエストします。
curl -o local-download-test2.wav http://localhost:8787/files/test2
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 134M 100 134M 0 0 405M 0 --:--:-- --:--:-- --:--:-- 404M
ファイルを確認します。
$ if cmp -s test2.wav local-download-test2.wav; then
echo "一致しています"
else
echo "一致していません"
fi
一致しています
つまり、wrangler dev でテストしても気が付けないということです。
wrangler dev は万能ではない
Commands
https://developers.cloudflare.com/workers/development-testing
上記に詳しいですが、wrangler dev は Miniflare で動いています。
また、このような記載があります。
Miniflare is a simulator for developing and testing Cloudflare Workers ↗. It's written in TypeScript, and runs your code in a sandbox implementing Workers' runtime APIs.
https://developers.cloudflare.com/workers/testing/miniflare
Miniflare はシミュレータであり、サンドボックスであり、Workers' runtime APIsを実装している。
とのことで「本物のWorkers' runtime APIsとは異なる部分があるよ。」と読めます。
今回は memory の limit がここに実装されていないということが分かりました。
参考情報
環境
M2 Mac Mini
Sequoia バージョン 15.6.1
$ node -v
v20.10.0
$ npm --version
10.2.5
参考文献など
-
一般的にはアップロードからやったほうが綺麗ですがその場合、先にrequest-limitsに到達してしまい、
今回説明したいことと離れますので、今回はダウンロードで実装します。
アップロード機構の代わりとして、ファイルは cloudflare dashboard から r2 オブジェクトストレージに直接格納します。
この記事の反響があれば次回はCloudflare Workers の Limits にふれる - request 編を書きます。 ↩︎ -
Cloudflare Workers で API を提供するなら、hono が一番楽です。(個人の感想です。が、確度は高いです。) ↩︎
Discussion