Lambda Web Adapter を LMI で動かしたときの挙動について調査
LMI に対応した Lambda Web Adapter で、言語ごとの書き分けがどこまで抽象化されるのかを、FastAPI での実測(プロセス数・同時数の上限・グローバル変数の混線)から確かめる。
- 公開日
はじめに
前回のLambda マネージドインスタンス(LMI)についての調査では、LMI を導入することで 1 つの実行環境(コンテナ)内で複数リクエストが同時に処理されるようになり、ランタイム(言語)ごとに考慮すべき並行処理の挙動が異なる点に触れました。
この違いによって実際に起きる問題については、AWS のトラブルシューティングドキュメントでもランタイムごとに注意点が挙げられています。
| ランタイム | 症状 | 原因 |
|---|---|---|
| Java | 負荷時に誤った結果を返す、競合状態が起きる | 複数のスレッドが同時にハンドラを実行し、共有状態がスレッドセーフでない |
| Node.js | 別のリクエストのデータを返す、データが壊れる | 同一ワーカースレッド上でグローバル変数が共有され、非同期処理が制御を手放したときに他の呼び出しが書き換える |
| Python | /tmp のファイルから誤ったデータを読む |
複数のプロセスが /tmp を共有し、同じファイルへの同時書き込みでデータが壊れる |
Java や Node.js では「メモリ(変数や状態)の共有」が主な課題ですが、Python では各リクエストが別のプロセスで動くため、「ファイルシステムの共有」で問題が発生しやすいのが特徴です。こうした言語ごとの並行処理モデルの違いを意識した書き分けは、アプリケーション開発者にとって大きな負担となります。
そこで、「既存の Web アプリをそのまま動かす Lambda Web Adapter (LWA) を使えば、こうした言語ごとの書き分けや複雑さを抽象化できるのではないか?」と考えました。そして実際、LWA は 2026 年 2 月のアップデート(PR #625)で正式に LMI に対応しています。
今回は、LWA を LMI 環境で実際に動かし、FastAPI を用いた実測(プロセス数、同時数上限、グローバル変数の混線)を通して、LWA がどこまで言語側の差分やマルチリクエストの複雑さを抽象化してくれるのかを検証しました。
(出典: Troubleshooting Lambda Managed Instances、2026-09-25 確認)
そもそも LWA とは?
本題に入る前に、Lambda Web Adapter(LWA)について簡単に復習しておきます。
LWA は、AWS がオープンソースで提供している Lambda Extension(拡張機能)です。Lambda レイヤーとして追加するか、コンテナ(Dockerfile)内にバイナリを 1 行コピーするだけで利用できます。これを使うことで、FastAPI、Express、Next.js、Gin といった通常の Web アプリケーションを、コードをほぼ変更することなく Lambda 上で動かすことができます。
通常の Lambda では、API Gateway や ALB から渡される JSON 形式のイベント(event オブジェクト)をハンドラー関数で受け取って処理コードを書く必要があります。
しかし LWA を導入すると、LWA が Lambda 実行環境内で「リバースプロキシ」として振る舞います。 外部からのイベントを標準的な HTTP リクエストに自動変換して同一環境内の Web サーバー(FastAPI 等)に転送し、Web サーバーからのレスポンスを、Lambda の応答の形式に戻して返してくれます。
つまり、 「Lambda 特有のハンドラー記述を不要にし、実行環境内の Web サーバーをそのまま呼び出せるようにする仕組み」 が LWA です。
イベントの受け取り方の比較図
通常の Lambda(①)に LWA を足すと(②)、イベントの受け取り方が以下のように変わります。
図のとおり、LWA を導入しても、利用者からの HTTP リクエストは Amazon API Gateway でいったんイベント(JSON)に変換されてから Lambda に届きます。 LWA は、そのイベントを受け取ってもう一度 HTTP リクエストに戻し、Web サーバーへ渡しています。
例として、GET /inflight?ms=800 というリクエストは、HTTP API(ペイロード形式 2.0)では次のようなイベントになります(抜粋)。
{
"version": "2.0",
"rawPath": "/inflight",
"rawQueryString": "ms=800",
"headers": { "user-agent": "curl/8.5.0" },
"requestContext": {
"http": { "method": "GET", "path": "/inflight" }
},
"isBase64Encoded": false
}
Lambda の関数が受け取るのは、生の HTTP ではなく、このようなイベントです。 関数 URL で HTTP を直接受けた場合も、同じ形式のイベントに変換されてから関数に届きます。 そのため、HTTP → イベント → HTTP という変換を 1 往復挟むことになりますが、その代わりに Web サーバー側のコードは Lambda を意識せずに書くことができるようになります。
(出典: Create AWS Lambda proxy integrations for HTTP APIs、Invoking Lambda function URLs、2026-09-26 確認)
LWA の LMI 対応状況について
LWA の LMI 対応に伴い、公式リポジトリには FastAPI を LMI 上で動かすサンプル(fastapi-response-streaming-lmi)が公開されています。つまり、LWA を LMI 上で動かすこと自体は、すでに AWS の公式リポジトリがサンプルを公開している構成 です。
LWA を Lambda 関数に組み込む方法は主に以下の 2 パターンがありますが、いずれの構成も LMI 環境で動作します(根拠は下の表のとおり、1 は公式サンプル、2 は今回の実測です)。
| # | 載せ方 | LMI で動くことの根拠 |
|---|---|---|
| 1 | zip + レイヤー(AWS_LAMBDA_EXEC_WRAPPER=/opt/bootstrap) |
公式サンプル |
| 2 | コンテナイメージ(/opt/extensions/ にバイナリを同梱) |
今回の実測 |
今回の検証ポイント
公式サンプルの README 等で示されているのは「1 つの実行環境(コンテナ)で複数の同時リクエストを処理できる」というプラットフォーム側の挙動までであり、「その内部でアプリケーション(FastAPI)がどのように振る舞うのか」までは言及されていませんでした。
そこで本記事では、LWA を挟むことでランタイム固有の並行処理の考慮事項がどこまで抽象化されているのかを、以下の 3 つの観点から実測・検証します。
| # | 検証項目 | 理由・着眼点 |
|---|---|---|
| 1 | 同時実行数の上限制御 | LMI 側の最大同時設定が Web アプリまで正しく伝搬するか。溢れたリクエストはどう処理されるか? |
| 2 | 実行環境内のプロセス構成 | リクエスト処理時に Python ランタイム特有のプロセス分離(マルチプロセス)が維持されるか? |
| 3 | グローバル変数の混線(State Leak) | 通常の Web フレームワーク(FastAPI)と同じ感覚で実装した際、リクエスト間で状態が混線しないか? |
(出典: aws-lambda-web-adapter の examples/fastapi-response-streaming-lmi、2026-09-26 確認)
実測(LMI 上での同時実行の挙動)
検証環境と条件
東京リージョン(ap-northeast-1)にて、LWA を同梱したコンテナイメージの関数を LMI 環境で作成し、計測を行いました。 条件は以下のとおりです。
| 項目 | 設定 |
|---|---|
| インスタンス | m7i.large(2 vCPU・8 GiB) |
| 関数のメモリ | 2 GB |
1 実行環境あたりの上限(PerExecutionEnvironmentMaxConcurrency) |
3 |
| アプリ | FastAPI + Uvicorn(ワーカー 1 つの既定の起動) |
上限設定はあえて「3」という小さな値に絞っています。 上限が正しく機能しているのか、あるいは単にリクエストが複数の実行環境に分散されただけなのかを見分けるためです。
検証結果のサマリ
実測した結果、LWA を挟んでも、LMI の同時実行はそのままアプリに届くことが分かりました。
3 つの検証項目に対する実測結果の概要は以下のとおりです。
| # | 検証項目 | 結果 |
|---|---|---|
| 1 | 同時実行数の上限制御 | 設定値どおり 3(溢れた分は待たずに HTTP 429) |
| 2 | 実行環境内のプロセス構成 | アプリ(Uvicorn)は 1 プロセス・1 スレッド(既定の 1 ワーカー構成) |
| 3 | グローバル変数の混線(State Leak) | 9 本中 6 本で混線 |
検証の詳細
1. 同時実行数の上限制御(#1)
検証時、実行環境は 3 つ立ち上がっていました(1 環境あたりの同時上限は 3 のため、この時点で全体として同時に処理できる限界は 3 環境 × 3 本 = 最大 9 本 です)。
この状態で、1 本あたり 800 ms かかるリクエストを並列で同時に送信した実測結果は以下のとおりです。
| 同時リクエスト数(全体) | 成功数 | 失敗数(429エラー) | 挙動の内訳 |
|---|---|---|---|
| 6 本 | 6 | 0 | 最大数(9 本)以下のため全件成功 |
| 12 本 | 9 | 3 | 最大数(9 本)が成功し、溢れた 3 本(12 - 9) がエラー |
| 30 本 | 9 | 21 | 最大数(9 本)が成功し、溢れた 21 本(30 - 9) がエラー |
■ スロットリング(HTTP 429)の動作図
以下「アクセス」や「まとめて 12本」,「まとめて 30本」を押すと、3つの実行環境(各3枠)へリクエストが届き、約0.8秒で処理されて枠が空きます。
枠が埋まっているときに届いたリクエストは、順番待ちせずにすぐ HTTP 429 で弾かれます。
▶ まず「アクセス」を押してください(1 回で 1 本送ります)。何度も続けて押すか「まとめて 30 本」を押すと、上限を超えた分が 429 になります。
実行環境 1同時上限 3
実行環境 2同時上限 3
実行環境 3同時上限 3
上限を超えた分(待たずに返す)
実測のとおり、上限を超えた分は順番待ち(キューイング)されず、この時点の処理限界(9 本)を超えたリクエストはその場で即座に HTTP 429 (Too Many Requests) として返却されました。
また、12 本と 30 本のときは、3 つの実行環境のどれもが同時に 3 本を処理しており、1 環境あたりの上限どおりに頭打ちになっていました。
なお、実行環境内の環境変数 AWS_LAMBDA_MAX_CONCURRENCY にも 3 が入っており、LMI の上限設定がアプリからも環境変数として見えていました。
2. 実行環境内のプロセス構成(#2)とグローバル変数の混線(#3)
実行環境ごとに見ると、アプリからの応答はすべて同一の PID(プロセスID)・同一のスレッドから返ってきています。
つまり、リクエストごとに別々のプロセスが立ち上がるのではなく、既定の 1 ワーカー構成では 1 つの Uvicorn プロセスが複数のリクエストを交互に(async/await で)処理している状態 になります。
このような構造のため、リクエスト間で共有される変数(グローバル変数)にデータを保持してしまうと、処理を交互に進めている間に別のリクエストによって値が上書きされ、混線が発生します。
「値を書き込んでから 300 ms 待ち、読み戻す」という処理を並列で同時に送信した実測結果は以下のとおりです。
| 同時リクエスト数(全体) | HTTP 成功 | 429 | うち混線(他データに書き換え) | 挙動の内訳 |
|---|---|---|---|---|
| 6 本 | 6 | 0 | 3 本 | 成功した 6 本のうち 50% でデータが混線 |
| 30 本 | 9 | 21 | 6 本 | 成功した 9 本のうち約 67%(6 本)でデータが混線 |
■ グローバル変数の混線の動作図
例として、ログイン中のユーザー名をグローバル変数 current_user に保持し、DBの応答を待ってから画面に表示する処理を考えます。
以下の「2人が同時にアクセス」を押すと、田中さんの画面に佐藤さんの名前が表示されてしまう現象をステップ順に確認できます。「1人ずつアクセス」を押した場合は、どちらの画面にも正しく本人の名前が表示されます。
▶ まず「2 人が同時にアクセス」を押してください。
田中さんのリクエストログイン中: 田中
未送信
佐藤さんのリクエストログイン中: 佐藤
未送信
実行環境(Uvicorn)PID 16 / スレッド 1
田中さんの画面返ったレスポンス
(まだ返っていない)佐藤さんの画面返ったレスポンス
(まだ返っていない)段階 0 / 4
送り方のボタンを押すと段階 1 を表示します。以後は「次へ」で 1 段階ずつ進みます。
■ 手元の Uvicorn(FastAPI 単体)との比較
この混線が「LWA 固有のバグ」なのか「Web アプリ(Uvicorn)本来の挙動」なのかを切り分けるため、LWA や LMI を通さず、全く同じアプリを手元の Uvicorn で直接動かして比較しました。
結論、 プロセス・スレッドの数と、同時に送ると混線するという挙動は、手元の Uvicorn と一致 していました(件数は送った本数や条件が違うため比べていません)。
この実験からも、LWA は隔離などの特別な処理を行わず、届いた呼び出しを「そのまま同時の HTTP リクエストとして Web サーバーへ受け渡しているだけ」であることが確認できます。
| 検証目的 | 検証項目 | 手元での実行結果 (FastAPI + Uvicorn) |
|---|---|---|
| 並行処理(async)されているか? | 20 本 × 800 ms の処理時間 | 0.84 秒(直列なら 16 秒かかるため並行処理されている) |
| プロセス分離が維持されているか? | 応答した PID / スレッド数 | 1 / 1(単一プロセス・単一スレッド) |
| 同時に届くとデータが混線するか? | グローバル変数の混線(20 本同時処理の場合) | 19 / 20 件 で混線発生 |
| 順番に届く場合は混線しないか? | グローバル変数の混線(1 本ずつ処理の場合) | 0 / 1 件(混線は発生しない) |
LWA × LMI を使うときの注意点について
今回の構成(Python・FastAPI・Uvicorn の既定 1 ワーカー)の実測から分かった注意点は、主に以下の 2 つです。
| # | 注意点 | 実測で分かったこと | 必要な対応 |
|---|---|---|---|
| 1 | 同時実行数の上限 | 上限を超えたリクエストは待たずに HTTP 429 で返る | 呼び出し側での再試行 |
| 2 | リクエストの同居 | 同時に届いたリクエストが 1 つのプロセスに同居し、グローバル変数に置いたリクエストごとの値が上書きされる | リクエストごとの値をグローバル変数に置かない作り |
1. 同時実行数の上限と再試行
同時実行の上限を超えたリクエストは、順番待ちせずに即座に HTTP 429 が返ります。 そのため、呼び出し元(フロントエンドやプロキシ層)で、指数バックオフなどの再試行制御を入れておく必要があります。 再試行すると同じリクエストが 2 回処理されうるため、書き込みを伴う処理では、同じリクエストを 2 回受けても結果が変わらない作り(冪等性)が前提です。再試行で足りない場合は、同時実行数の上限や vCPU の上限を見直します。
2. リクエストの同居とグローバル変数
LWA が保護するのは「Lambda イベント ↔ HTTP」の変換層までであり、プロセスの境界から先には関与しません。 LWA はアプリとは別のプロセス(拡張機能)として動き、同時に届いた呼び出しを、そのまま同時の HTTP リクエストとして localhost の Web サーバーへ渡すところまでを担っています。
そのため、Web サーバーの中でリクエストをどう捌くかは、Web サーバー自身の仕組み(今回は Uvicorn の async/await)で決まります。
リクエストごとの値(リクエストのたびに書き換わる値)はグローバル変数に置かず、ハンドラーの中のローカル変数などで持つ作りが必要です。
なお、同時処理のしかたは載せる Web サーバーとその設定(ワーカー数など)で変わるため、Python・Uvicorn 以外の構成では個別の確認が必要です。
なお、公式サンプルのアプリも、リクエスト ID をハンドラーの中のローカル変数で生成しており、この問題に当たらない書き方になっています。
3. Python の場合に加わる注意点
さらに Python の場合は、ネイティブの LMI から LWA に切り替えることで、2 の注意点が新しく加わります。 これは実測の結果と、公式ドキュメントに書かれたネイティブの Python ランタイムの仕組みを比べて分かったことです(ネイティブの LMI は今回実測していません)。
- ネイティブの LMI(Python): 同時に届いたリクエストは別々のプロセスで処理されます(公式ドキュメント)。そのため、同時に処理されているリクエストどうしでグローバル変数が混線することはなく、気をつけるべきはプロセスをまたいで共有される
/tmpなどでした。ただし、プロセスは次のリクエストで使い回されるため、グローバル変数に置いた値は、同時ではない次のリクエストに残ります。 - LWA を挟んだ LMI(FastAPI + Uvicorn): 既定の 1 ワーカー構成では、同時のリクエストが 1 つのプロセスに同居し、グローバル変数が混線しました(実測)。ワーカー数を増やした構成は今回試していません。
■ ネイティブの LMI と LWA の比較図(グローバル変数が混線するか)
結論として、1 つの実行環境の中で見ると、ネイティブの LMI(①)では同時に処理されるリクエストごとにグローバル変数が別になるため混線しませんが、LWA を挟んだ LMI(②)ではその実行環境に同時に届いたリクエストが 1 つのグローバル変数を共有するため混線します。 LWA が担うのは、図の点線(プロセスの境界)より左側の変換までです。
一方、Java や Node.js は、ネイティブの LMI の時点で 1 つのプロセスの中で同時に処理されるため、グローバル変数は最初から共有されます(冒頭の公式の不具合例の表のとおり)。 そのため、Python のように「LWA に切り替えることで、グローバル変数の注意点が新しく加わる」形にはなりません。 ただし、同時処理のしかた(スレッドか非同期か、ワーカーをいくつ持つか)は、ネイティブのランタイムと LWA に載せる Web サーバーとで変わりえます。今回確認したのは Python・Uvicorn の構成だけなので、ほかの言語では個別の確認が必要です。
(出典: Python runtime for Lambda Managed Instances、Lambda Managed Instances runtimes、2026-09-25 確認)
4.その他の制約(実測外)
実測とは別に、公式ドキュメントから分かる制約もあります。
- タイムアウトは 15 分のまま: 関数 URL や API Gateway 経由の Web アプリは同期呼び出しとなるため、上限は 15 分のままです(API Gateway を挟む場合は、API Gateway 側の統合タイムアウトが先に効きます)。90 分まで拡張できるのは非同期と ESM だけなので、Web アプリとして使う限り 90 分拡張の話は関係ありません。
- Python でのストリーミングは LWA かカスタムランタイムが必要: Lambda のマネージドランタイムでレスポンスストリーミングに対応しているのは Node.js のみ です。Python などの他言語でストリーミングを行いたい場合は、公式ドキュメントでも「カスタムランタイムか LWA を使うこと」と推奨されています。
(出典: Response streaming for Lambda functions、2026-09-25 確認)
まとめ
LWA は LMI への対応を公式に完了しています。「zip + レイヤー」は公式サンプルで示されており、「コンテナイメージ」は今回の実測で動作を確認しました。
ただし、LWA が抽象化してくれるのは「Lambda と HTTP の変換」まで であり、同時実行に伴う並行処理の面倒までは消してくれません。 実測からは、同時実行数の上限を超えたときの再試行と、リクエストごとの値をグローバル変数に置かない作りが必要だと分かりました。
使い分けの判断材料としては、Python でイベントハンドラーで足りるならネイティブの LMI、既存の Web アプリをそのまま移したい場合や、Python でレスポンスストリーミングをしたい場合は LWA を検討する、という整理になります。 ただし、どちらを選んでも、リクエストごとの値をグローバル変数に置かない作りは前提です。性能や運用の比較は今回していません。
そのため、LWA を導入することで得られる価値は「並行処理の考慮事項が不要になること」ではなく、あくまで 「Lambda 固有のイベントハンドラー作法を意識する代わりに、Amazon ECS や Amazon EC2 等の Web アプリ開発で用いられる標準的な原則(ステートレス設計・リクエストごとの状態管理)をそのまま適用できること」 が、LWA × LMI を使う利点だと理解しました。