はじめに
本番で Supabase Edge Functions を叩いたら、見慣れない HTTP ステータスが返ってきました。
Error: submit-image-job failed (HTTP 546)
546。RFC にも無ければ、ブラウザの DevTools に出てもピンと来ない番号です。Supabase のダッシュボードのログには「request cancelled」としか出ていない。タイムアウト? メモリ? CPU? — クライアント側からは 何が原因で落ちたのか全く区別がつきません。
この記事は、この 546 の 正体がどこに書かれていて、ローカルで再現するには何を動かせばよく、本番で吐いたときに原因を特定するには何を見れば良いのか を、実際にやって突き止めたときの記録です。
Issue 側には業務寄りの対応方針(ペイロード圧縮・バッチ改善)を書いてあるので、本記事は純粋に Edge Functions のランタイム挙動にフォーカスします。
結論(先に)
- HTTP 546 は Supabase Edge Functions 独自のステータス。worker が supervisor にリソース制限でキャンセルされた ことを表す
- 正体は Edge Functions のサイドに OSS として公開されている supabase/edge-runtime の中にある。ローカルの Docker イメージの
Cmdにもそのまま埋まっている ので、docker inspectで読める - メモリ上限超過でも CPU ハード上限超過でも、クライアントに返る HTTP は同じ 546。区別はサーバー側ログでしかできない
- ログは Management API の
function_logsに乗り、memory_used/cpu_time_usedが入っている(ただし数時間で消える) - ローカルで再現するには、Supabase CLI ではなく
edge-runtimeイメージを standalone でdocker runして合成関数をマウント するのが確実 - 今回は本番に 40.4 MB のペイロードを投入して再現し、
function_logsにMemory limit exceeded (257.3MB)を確認できた
546 はどこで定義されているのか
Edge Functions の Runtime は Supabase がフォークした Deno ベースのランタイムで、OSS として supabase/edge-runtime に公開されています。ダッシュボードや CLI が内部で呼んでいる実体は、このコンテナです。
エラー → HTTP マッピングは Runtime のコンテナ起動スクリプトに JS でベタ書きされていて、Supabase CLI が引っ張ってくる edge-runtime の Docker イメージを docker inspect すると、その 起動スクリプトがそのまま Cmd に入っている のが見えます。
$ docker inspect supabase/edge-runtime:v1.73.3 \
| jq -r '.[0].Config.Cmd[]' \
| grep -E "(546|503|WorkerRequestCancelled|memoryLimitMb)"
結果(抜粋・整形):
if (error instanceof WorkerRequestCancelled) {
return new Response(..., { status: 546, ... });
}
if (error instanceof InvalidWorkerResponse) {
return new Response(..., { status: 500, ... });
}
if (error instanceof WorkerBootError) {
return new Response(..., { status: 503, ... });
}
// ...
const workerOptions = {
memoryLimitMb: 256,
cpuTimeSoftLimitMs: 1000,
cpuTimeHardLimitMs: 2000,
workerTimeoutMs: 400000,
...
};
見たいものが全部あります。要点は 3 つ。
-
HTTP 546 =
WorkerRequestCancelled。worker が途中で supervisor に殺されたことを意味する - メモリ / CPU / wall-clock のどれで殺されたかは、この HTTP ステータスには乗ってこない
-
リソース上限は
memoryLimitMb=256,cpuTimeHardLimitMs=2000(2026-04 時点、v1.73.3)
つまり 546 を受け取った時点では、「worker 側で何かが上限に触れた」ということまでしか分からない、というのが公式仕様なんですね。
ローカルで再現するには
Supabase CLI の supabase functions serve でも Edge Runtime は動きますが、ユーザー関数のコードを介在させる以上、「純粋に 546 を再現する最小構成」を作りづらいです。もっと直接やります。
合成関数
リソースを強制的に食う関数を 2 つ用意します。
// repro-memory/index.ts
Deno.serve(async (req) => {
const body = await req.text(); // クライアントから大きいボディを送り込む
// さらに parse を重ねてメモリを積む
const parsed = JSON.parse(body);
const copies = Array.from({ length: 4 }, () => JSON.parse(JSON.stringify(parsed)));
return new Response(JSON.stringify({ size: copies.length }));
});
// repro-cpu/index.ts
Deno.serve(() => {
const start = Date.now();
while (Date.now() - start < 5000) { /* busy */ }
return new Response("done");
});
edge-runtime を standalone で起動
docker run --rm -p 9000:9000 \
-v $(pwd)/functions:/home/deno/functions \
supabase/edge-runtime:v1.73.3 \
start --main-service /home/deno/functions/main -p 9000
main サービスは supabase/edge-runtime の examples にあるディスパッチャをそのまま使えば OK です(関数名で /home/deno/functions/<name>/index.ts にルーティングする薄いもの)。
叩く
メモリを踏ませる側:
# 100 MB の JSON を送りつける
python3 -c "import json; print(json.dumps({'x': 'a' * 100_000_000}))" \
| curl -sS -o /dev/null -w "%{http_code}\n" \
-H 'Content-Type: application/json' \
--data-binary @- \
http://localhost:9000/repro-memory
# => 546
CPU を踏ませる側:
curl -sS -o /dev/null -w "%{http_code}\n" http://localhost:9000/repro-cpu
# => 546
ログで区別する
どちらも クライアントには同じ 546 が返ります。区別は docker logs でしか付きません。
# メモリ側
event=uncaught_exception memory limit reached for the worker
# CPU 側
event=worker_cpu_time_hard_limit_reached
「546 が出た」だけでは原因がわからない、というのは実装上の仕様だと確定できました。
本番の 546 を function_logs で追う
Supabase 本番の worker は手元には居ないので、docker logs は見られません。代わりに Management API の function_logs を使います。
curl -H "Authorization: Bearer $SUPABASE_ACCESS_TOKEN" \
"https://api.supabase.com/v1/projects/$PROJECT_REF/analytics/endpoints/logs.all?sql=..."
このエンドポイントに SQL を投げると、各関数の実行ごとに以下の情報が返ってきます。
-
function_id/execution_id -
cpu_time_used(ms) -
memory_usedの内訳:total/heap/external(MB) - event:
uncaught_exception/worker_cpu_time_hard_limit_reached/memory_limit_exceededなど
今回は、事故と同じペイロード(13 items, 40.4 MB)をフロントから再投入して、直後に function_logs を回収したところ次が出ました。
submit-image-job (fn=882dae97)
cpu=300ms mem total=135.6MB heap=92.3MB ext=43.3MB → 成功
poll-image-jobs (fn=0cd89453)
cpu=175ms mem total=257.3MB heap=173.2MB ext=84.1MB → Memory limit exceeded
memoryLimitMb=256 の世界で、poll-image-jobs が 257.3 MB に達してキルされている、という事実がログ上で確定しました。クライアントに返った HTTP は例のごとく 546 のみ です。
落とし穴: function_logs は短命
ひとつ注意があります。function_logs は 数時間で消えます(保持期間はプランとリージョンで変動)。本番で 546 を踏んだら、ユーザーから報告が来てから調査を始めると、もうログが消えている ことが普通にあります。
対策としては、関数側で意識的に構造化ログを出して自分側のログ基盤に転送する、あるいは Management API を定期ポーリングして保全する、などが必要になります。「Supabase 側に一次情報が揃っている」状態を永続させない設計だと覚えておくべきです。
546 を踏んだときの対応フロー(テンプレ)
今回の調査で身に付いた、546 を踏んだときの初動フローをまとめておきます。
- クライアント側の HTTP は 546 以上のことは教えてくれないと諦める
- 直後に Management API の
function_logsを叩いてmemory_used/cpu_time_used/ event を確認する(時間勝負) -
memory_used.totalが 256 MB 近辺なら メモリ系、cpu_time_usedが 2000 ms 近辺なら CPU ハード上限 - ローカルで同じペイロードを
edge-runtimestandalone に流し、docker logsで最終確認する - アプリ側は「ボディが大きいこと」「関数内でコピーが増えること」「同期重処理を関数内でやっていること」の 3 つを疑う
メモリ原因なら、だいたい クライアントが同じ参照データを毎 item インラインコピーして送っている ことが多いです。今回も 1 item あたり 3.5 MB の base64 を 13 items 分重複して送っていました。storagePath や touchIndex のような参照渡しインターフェースが関数側にあるならそちらを使うべきで、Edge Functions の 256 MB 制限は「素直に base64 を全部詰める」発想だと普通に触れる、というのが今回の実感です。
まとめ
- Supabase Edge Functions の HTTP 546 =
WorkerRequestCancelled。メモリ / CPU / wall-clock のどれでキャンセルされたかはクライアント側からは区別できない - 定義は supabase/edge-runtime の OSS に公開されており、ローカルの Docker イメージの
Cmdにそのまま埋め込まれているのでdocker inspectで読める -
memoryLimitMb=256,cpuTimeHardLimitMs=2000という具体値もそこから拾える - 原因特定は Management API の
function_logsがほぼ唯一の確実な情報源。数時間で消えるので即座に回収する - ローカル再現は
edge-runtimeを standalone でdocker run+ 合成関数でcurlが最速 - 大きなペイロードを JSON でそのまま Edge Function に送り込む設計は、256 MB 制限に普通に触れる。参照渡しのインターフェースを検討する
「546 を踏んだけど何が悪いのか分からない」段階の人に、原因の切り分け手順として届けば幸いです。