[AWS Lambda] Web Functions は I/O 待ちの負荷で実行環境がいくつ立つのか、スロットリングまで実測してみました

[AWS Lambda] Web Functions は I/O 待ちの負荷で実行環境がいくつ立つのか、スロットリングまで実測してみました

AWS Lambda Web Functionsの実行環境スケーリングについて、I/O待ち主体の負荷で実測してみました。関数作成直後と暖めた後で環境数や429エラーの出方がどう変わるのか、maxEnvironmentsやrateLimitの上限に当たったときの挙動を確認してみた結果を共有させてください
2026.11.06

1 はじめに

製造ビジネステクノロジー部の平内(SIN)です。

2026 年 10 月に GA になった AWS Lambda Web Functions は、1 つの実行環境が複数のリクエストを同時に処理します。環境あたりの同時数は Lambda が動的に決め、上限は既定で 64 です(maxConcurrencyPerEnvironment で 1〜128)。環境の数は需要より先に増やして余裕(headroom)を持たせる、とドキュメントに書かれています。

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-scaling.html

ここ、DevelopersIO では、CPU バウンドな処理で他のリクエストが待たされるかが、既に検証されています。

https://dev.classmethod.jp/articles/shuntaka-lambda-web-functions-nodejs-cpu-bound/

ただ、生成 AI の応答待ちのような I/O バウンドの負荷では、CPU はほとんど使わないので「待たされるか」は、問題になりにくいかも知れません。
代わりに「1 つの実行環境に何件まで入るのか」「環境はいくつ立つのか」「どこで 429 になるのか」あたりが気になってきます。

そこで今回は、指定した時間だけ待って返すだけの擬似 LLM を Web Functions に載せて、同時数を変えながら実行環境の数を数え、maxEnvironments と rateLimit の上限に当てて 429 の出方を実測してみました。

なお、計測は手元の AWS アカウントと Mac から行ったものです。Web Functions がどう動くのかを把握したいという個人的な興味で試したもので、本番を想定した負荷試験や、Web Functions の仕様を示すものではないことをご了承ください。

関数を作った直後と、一度動かした後では動き方が違ったので、同じ負荷を 2 つの状態でかけました。把握できた事項は以下です。

  • 一度動かした後は、環境の数が同時数に応じて増えました。同時 10 / 50 / 100 / 200 件で 1 / 2 / 3 / 5 環境、1 環境あたり 10〜40 件です
  • 作った直後は、1 環境に 2 件しか入らないところから始まりました。そのため最初の十数秒は、maxEnvironments 10 の prod でも同時 50 件以上で 429 が出ています。新しいリビジョンをデプロイしただけでは、この状態には戻りませんでした
  • maxEnvironments 2 では、一度動かした後は最初から 2 環境 × 64 件 = 128 件が通り、残りは即時に 429 でした。作った直後は、128 件に届くまで約 40 秒かかりました
  • rateLimit 100 は、どちらの状態でもほぼ同じでした。約 200 件通る秒と数件しか通らない秒が交互に現れます

2 スケーリングの仕様

今回の題材に関係するところを、公式ドキュメントから整理しました。

設定 置き場所 範囲 既定
maxConcurrencyPerEnvironment リビジョン 1〜128 64
maxEnvironments エンドポイント 2〜10,000 なし(アカウントの vCPU クォータのみ)
rateLimit エンドポイント 0、100〜1,000(100 刻み)、2,000〜10,000(1,000 刻み) なし(エンドポイントあたり 10,000 rps のクォータのみ)
  • スケールは 2 軸です。環境の数(maxEnvironments まで)と、環境あたりの同時数(Lambda が動的に決め、maxConcurrencyPerEnvironment を超えない)
  • 環境は需要より先に増やして余裕(headroom)を持たせ、使われていない環境は減らします(ゼロまで)
  • 1 環境は 2 vCPU を使い、アカウントの vCPU クォータはリージョンあたり 2,000 です
  • 上限に達して増やせず、既存の環境も満杯のときは、キューせずに即 429 を返します。新しい環境が初期化中なら、リクエストは待ちます
  • rateLimit を超えた分も 429 です。環境に届く前に適用され、環境に余裕があっても 429 になります

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-scaling.html

429 と 503 には lambda-web-error-type ヘッダーで原因が入ります。ヘッダーの値と、増える CloudWatch メトリクスの名前が揃っていないので、表にしておきます。

原因 HTTP lambda-web-error-type 増えるメトリクス(AWS/Lambda/Web/Usage)
rateLimit 超過 429 Platform.EndpointRequestThrottle EndpointRequestThrottles
maxEnvironments 到達 429 Platform.EndpointMaxEnvThrottle EndpointActiveEnvironmentThrottles
アカウントのリクエストレートのクォータ 429 Platform.AccountRequestThrottle AccountRequestThrottles
アカウントの vCPU クォータ 429 Platform.AccountActiveVCPUArm64Throttle AccountActiveVCPUArm64Throttles
同時初期化が多すぎる 429 Platform.EndpointConcurrentInitThrottle 増えない
rateLimit が 0 503 Platform.EndpointDisabled
一時的な負荷制限(load shedding)または内部エラー 503 ヘッダーなし

429 はどれか 1 つのメトリクスにだけ数えられ、あわせて AWS/Lambda/Web の HTTPResponseStatusClass4XX にも数えられます。ヘッダーのない 503 は、バックオフとジッターを付けて再試行するよう書かれています。

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-troubleshooting.html

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-monitoring-metrics.html

関数を作った直後の動きについては、次のように書かれています。

  • 新しい Web Function では、環境あたりの動的な上限を控えめな値から始め、リソース使用量から学習して上げる
  • 学習した値は、ゼロ環境にスケールしても保持される
  • そのため、最初のロードテストは後の実行より多くの環境を使うことがある。性能を測るならロードテストは複数回行い、後の回の結果を使う

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-scaling.html#web-functions-scaling-examples-dynamic-limit

3 構成

アプリは Hono で書いた 1 本だけです。GET /wait?ms=N で N ミリ秒 setTimeout して返します。

CPU はほとんど使わず、生成 AI の応答を待っている状態に近い負荷になります。あわせて、環境の識別子と、その環境が今処理中の件数(inflight)を返すようにしました。

Github app/src/index.ts

// 要点抜粋
const ENV_ID = randomUUID().slice(0, 8) // 起動時に 1 回だけ作る。同じ環境なら同じ値
let inflight = 0

app.get('/wait', async (c) => {
  const ms = Number(c.req.query('ms') ?? 0)
  inflight++
  try {
    await new Promise((r) => setTimeout(r, ms))
    return c.json({ ms, env: ENV_ID, inflight })
  } finally {
    inflight--
  }
})

Web Function は 1 つで、maxConcurrencyPerEnvironment は既定の 64 のままです。maxEnvironments と rateLimit はエンドポイントの設定なので、同じ関数にエンドポイントを 3 つ付けました。

エンドポイント 設定 見たいこと
prod maxEnvironments 10 同時数を変えると環境はいくつ立つか
maxenv2 maxEnvironments 2 環境数の上限に当たったときの 429
rate100 rateLimit 100 毎秒のリクエスト数の上限に当たったときの 429

エンドポイント 1 つの設定を負荷ごとに変えても試せますが、条件ごとにエンドポイントを分けました。URL を変えるだけで条件を切り替えられ、CloudWatch のメトリクスもエンドポイント名ごとに分かれて、条件ごとに読めるためです。エンドポイントは、同じ関数のものでもそれぞれ独立にスケールします。

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-scaling.html#web-functions-scaling-interaction

prod に maxEnvironments 10 を付けたのは、費用の上限のためです。1 環境は 2 vCPU なので、最大でも 20 vCPU に収まります。

Github cdk/lib/stack.ts

// 要点抜粋
const revision = new lambda.CfnWebFunctionRevision(this, 'Revision', {
  functionName: fn.ref,
  buildConfig: { runtimeConfig: { runtime: 'nodejs24.x' }, codeConfig: { s3Object: { bucket: bucket.bucketName, key } } },
  serviceConfig: {
    executionRoleArn: role.roleArn,
    timeoutSeconds: 60,
    // maxConcurrencyPerEnvironment は指定しない(既定の 64)
    // リクエストごとの結果(platform.requestComplete)をログに出す
    telemetryConfig: { loggingConfig: { systemLogLevel: 'DEBUG' } },
  },
})
makeEndpoint('prod', { scalingConfig: { maxEnvironments: 10 } })
makeEndpoint('maxenv2', { scalingConfig: { maxEnvironments: 2 } })
makeEndpoint('rate100', { scalingConfig: { maxEnvironments: 10 }, throttleConfig: { rateLimit: 100 } })

4 計測方法

計測した環境は以下です。

項目 バージョン
ランタイム nodejs24.x
aws-cdk-lib / aws-cdk 2.273.0 / 2.1145
リージョン ap-northeast-1(東京)
実施日 2026 年 11 月 5 日(新しいリビジョンの確認のみ 11 月 4 日)

最初は、作った直後の関数に同時 10 / 50 / 100 / 200 件を順に流しました。すると最初の同時 10 件だけ、1 環境で足りるはずのところに 8 環境が立ち、その後の同時 50 件からは 1 環境あたり 25 件以上が入りました。ドキュメントにある「新しい関数は控えめな上限から始めて学習する」動きです。

この予備計測では、最初の同時 10 件を約 1 分で止めていて、「8 環境のまま変わらない」と結論しかけました。約 3 分に延ばすと、約 2 分で 1 環境に集約されています。

そこで本計測は、同じ負荷を次の 2 つの状態でかけました。

状態 作り方
作った直後 スタックを作り直し、一度もリクエストを送らずに負荷をかける。学習した値は同じ関数のエンドポイント間で共有されるので、負荷 1 つごとに作り直す
一度動かした後 prod に同時 10 件を約 3 分流したあと 7 分空け、実行環境がすべて止まってから負荷をかける。負荷と負荷の間も 7 分空ける

負荷は 6 種類です。同時 N 件の負荷は、待ち 5 秒のリクエストを同時に N 件投げ、全部返ったら次の N 件を投げる、を約 3 分(36 回)続けます。1 回だけだと、環境が増えている途中の瞬間しか見えないためです。

負荷 エンドポイント
同時 10 / 50 / 100 / 200 件 prod(maxEnvironments 10)
同時 200 件 maxenv2(maxEnvironments 2)
毎秒 300 件を 30 秒、待ちなし rate100(rateLimit 100)

負荷は手元の Mac から Node.js のスクリプトでかけ、レスポンスの status、lambda-web-error-type、環境の識別子、応答時間を JSON に保存しました。12 回分の作り直しと負荷は、シェルスクリプトで順に流しています(約 1 時間 45 分)。

Github scripts/load.mjs

# 同時 50 件、待ち 5 秒、36 回
$ node scripts/load.mjs --url <URL> --name n50 --concurrency 50 --ms 5000 --rounds 36

# 毎秒 300 件を 30 秒、待ち 0
$ node scripts/load.mjs --url <URL> --name rate100 --rps 300 --duration 30 --path /

5 一度動かした後

(1) 環境数

prod に同時 10 / 50 / 100 / 200 件をかけたときの、落ち着いた後の環境数です。429 はどれも出ていません。

同時数 環境数 1 環境あたり
10 1 10 件
50 2 25 件
100 3 33〜34 件
200 5 40 件

同時数が増えるほど環境も増え、1 環境あたりは均等に割られていました。この配分は約 3 分間変わりません。

上限の 64 件まで入ったのは、負荷をかけ始めた最初の約 5 秒だけです。環境が揃うまでの間は、立ち上がった環境に 64 件まで詰めていました(同時 200 件なら 64 / 64 / 64 / 8 件)。CloudWatch の EndpointActiveEnvironments も 1 / 2 / 3 / 5 環境で、クライアント側で数えた環境の数と一致しています。

(2) maxEnvironments

maxenv2 に同時 200 件をかけると、最初から最後まで 2 環境 × 64 件の 128 件が通り、残りの 72 件は 429 でした。

横に広げられないときは、1 環境に上限の 64 件まで積まれます。通った 128 件は、ドキュメントの「maxEnvironments × maxConcurrencyPerEnvironment」のとおりです。maxenv2 にはこれまでリクエストを送っていませんが、prod で暖めた分の学習が効いていました。

429 にはすべて lambda-web-error-type: Platform.EndpointMaxEnvThrottle が付き、応答時間は中央値で約 220 ms でした。lambda-web-logstream ヘッダーがなく、環境に届く前に即時に返されています。429 は合計 2,592 件で、CloudWatch の EndpointActiveEnvironmentThrottles と一致していました。

(3) rateLimit

rate100 に毎秒 300 件を 30 秒(9,001 件)かけた結果です。待ち 5 秒のリクエストでは 64 並列でも毎秒 13 件にしか届かないため、ここだけ待ちなしです。

結果 件数 lambda-web-error-type
200 2,636
429 6,107 Platform.EndpointRequestThrottle
503 258 ヘッダーなし

通ったのは、最初の 2 秒を除いて平均で毎秒 90 件で、rateLimit 100 を少し下回ります。ただし、秒ごとに見ると約 180 件通る秒と数件しか通らない秒が交互に現れました。ドキュメントにも「短いバーストは上限を超えて通ることがある」と書かれています。

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-scaling.html#web-functions-scaling-throttling

経過 200 429 503
0 秒 100 103 97
1 秒 19 222 59
2 秒 1 299 0
3 秒 183 115 2
4 秒 14 286 0
5 秒 183 117 0

503 は 258 件のうち 156 件が、最初の 2 秒に集中していました。実行環境がまだ 1 つもないところに毎秒 300 件が届き、rateLimit を通過した分のうち受け手のない分が 503 になったように見えます。

ヘッダーのない 503 は、ドキュメントでは「一時的な負荷制限(load shedding)または内部エラー」とされ、バックオフとジッターを付けた再試行が推奨されています。

6 作った直後

(1) 環境数

作った直後の prod に同時 10 / 50 / 100 / 200 件をかけたときの、最初と落ち着いた後の環境数です。

同時数 最初 落ち着いた後 落ち着くまで 429
10 4 環境 × 2 件 1 環境 × 10 件 約 50 秒 0
50 10 環境 × 2 件 2 環境 × 25 件 約 100 秒 30
100 10 環境 × 2 件 4 環境 × 25 件 約 2 分 85
200 10 環境 × 2 件 8 環境 × 25 件 約 105 秒 313

負荷をかけた直後は、どの同時数でも 1 環境に 2 件しか入りませんでした。同時 50 件以上では maxEnvironments の 10 環境まで一気に立ち、10 × 2 = 20 件を超えた分が 429(Platform.EndpointMaxEnvThrottle)になっています。一度動かした後なら 429 が出ない負荷です。

1 環境あたりの件数は約 15 秒で 10〜20 件まで上がり、429 はそこで止まりました。その後、約 2 分かけて 1 環境 25 件ずつに揃っています。一度動かした後の 33〜40 件まではこの 3 分では上がらず、同時 100 / 200 件では環境が 1 / 3 つ多いままでした。

なお、同時 10 件は、同じ条件の 11 月 3 日の計測では 8 環境が立ち、集約まで約 2 分かかっていました。最初に立つ環境の数や落ち着くまでの時間は、回によって差があるようです。

CloudWatch の EndpointActiveEnvironments(prod、1 分ごとの最大)です。前半の 4 つの山が作った直後、後半の 4 つが一度動かした後の同時 10 / 50 / 100 / 200 件です。19:52 の山は、一度動かした後の状態を作るためのウォームアップです。

作った直後は、いったん上限の 10 環境に張り付いてから下がっています。一度動かした後は、最初から平らです。

429 の件数も、CloudWatch の EndpointActiveEnvironmentThrottles(prod、1 分ごとの合計)でクライアント側の集計と一致していました。429 が出たのは 19:19・19:26・19:35 の各 1 分だけで、19:19 と 19:26 の間の線は、点をつないで描かれたものです。

(2) maxEnvironments

作った直後の maxenv2 に同時 200 件をかけたときの、通った件数の推移です。

開始からの時間 1 環境あたり 200 429
0 秒 2 4 194
16 秒 32 63 137
37 秒以降 64 128 72

計算上は 2 環境 × 64 件 = 128 件まで入るはずですが、最初は 4 件しか通らず、128 件に届くまで約 40 秒かかりました。429 は合計 3,061 件で、一度動かした後の 2,592 件より 469 件多く、この分が計算上は入るはずだった件数です。

CloudWatch の EndpointActiveEnvironmentThrottles(maxenv2、1 分ごとの合計)です。19:42〜19:45 が作った直後、20:43〜20:46 が一度動かした後で、どちらもクライアント側の集計と一致していました。差は最初の約 40 秒にしかないので、1 分ごとの合計では山の形はほぼ同じです。

(3) rateLimit

作った直後の rate100 に毎秒 300 件を 30 秒かけた結果です。

結果 件数 lambda-web-error-type
200 2,623
429 6,106 Platform.EndpointRequestThrottle 6,026 件、Platform.EndpointMaxEnvThrottle 80 件
503 271 ヘッダーなし

件数は一度動かした後とほぼ同じで、rateLimit は環境に届く前に効くため、作った直後かどうかの影響はほとんどありません。ただ、429 のうち 80 件は maxEnvironments 側でした。最初は 1 環境に 2 件しか入らないので、rateLimit を通った分の一部が環境に入りきらなかったようです。

(4) 502 Platform.CannotAssumeRole

作った直後の prod の同時 10 件と maxenv2 の同時 200 件では、負荷をかけた最初の 1 回に 502 が 2 件ずつ返りました。lambda-web-error-type は Platform.CannotAssumeRole で、ドキュメントでは「Lambda が関数の実行ロールを引き受けられなかった」とされています。

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-troubleshooting.html#web-functions-troubleshooting-error-types

スタックを作り直すたびに実行ロールも作り直していて、デプロイ完了の数秒後に負荷をかけていました。ロールの反映が間に合っていなかったのだと思いますが、確かめてはいません。2 回目以降は出ていないので、作ってすぐに受けるリクエストだけの話です。

(5) 新しいリビジョン

作った直後の状態に戻るのは、関数を新しく作ったときだけなのかも確かめました。一度動かした関数に、環境変数だけを変えた新しいリビジョンをデプロイし、maxenv2 に同時 200 件をかけています。

結果は、最初から 128 件が通りました。学習した値は、リビジョンではなく関数に付いているようです。

デプロイ時には、次の点を考慮しておくのが良いのでは、と思いました。

  • 気にするのは、関数を新しく作ったとき(新しい環境やステージを作る、スタックを作り直すなど)です。リビジョンのデプロイだけなら影響はありませんでした
  • 作った直後の十数秒は、maxEnvironments × 2 件しか受けられません。作ってすぐにバーストが来るなら、maxEnvironments に余裕を持たせておきます
  • 本番のトラフィックを流す前に、少しの負荷で暖めておくと避けられます。今回は同時 10 件を約 3 分流しただけで、その後はどのエンドポイントも最初から一度動かした後の動きになりました
  • コードや maxConcurrencyPerEnvironment を変えたリビジョンでの扱いは、確かめていません

7 最後に

今回は、I/O 待ち主体の負荷で Web Functions の実行環境がどう増減し、どこで 429 になるのかを実測してみました。実務で意識しておいたほうが良いなと思った点です。

  • maxEnvironments は、平常時の同時数だけでなく、関数を作った直後も含めて決めます。新しい環境を作る手順に、暖めてから本番のトラフィックを流す工程を組み込むのも手です
  • クライアント側では、429 とヘッダーのない 503 の両方を、バックオフ付きのリトライの対象にしておきます
  • 環境の数や性能を測るなら、ロードテストは 2 回以上流し、2 回目以降の結果を使います。ドキュメントもそう推奨しています

本稿はあくまで、待ち 5 秒・CPU ほぼ 0 という特定の負荷での一例として捉えてください。環境の数や 1 環境あたりの件数は Lambda 側の判断で、負荷の性質によって変わると思います。

この記事で使用したコードは、以下に置きました。

https://github.com/furuya02/lambda-web-functions-scaling-throttling

8 参考リンク

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事