はじめに
以前、AWS Lambdaの制限まとめ|処理可能なファイルサイズや実行時間など初心者向けに解説という記事を書きました。
前回の記事では、
- 最大実行時間は15分(900秒)
- メモリは128MB〜10GB
- 一時ストレージ(/tmp)は最大10GB
- デプロイパッケージはZIPで50MB、展開後250MB
- 同期レスポンスは6MB、環境変数は4KB
- 同時実行数はデフォルトで1,000
といった、Lambdaの主要な制限値を一覧としてまとめました。
ただ、前回は「制限値がいくつか」を紹介するところで終わっていて、
- 「同時実行数の1,000という数字は、具体的にどうスケールしていくのか」
- 「よく聞く『コールドスタート』って結局何が起きているのか」
- 「SnapStartって最近よく見るけど、Provisioned Concurrencyと何が違うのか」
といった、一歩踏み込んだ疑問には答えられていませんでした。そこで今回は、前回の記事からもう一歩踏み込んで、Lambdaの制限まわりの「仕組み」の部分を調べてみました。
この記事はこんな方におすすめ
- 前回の記事でLambdaの制限値は分かったので、もう少し仕組みを知りたい方
- 同時実行数のスケーリングやスロットリングの挙動を理解したい方
- コールドスタートの正体や対策を知りたい方
- SnapStartとProvisioned Concurrencyの違いがよく分からない方
- Lambdaのレイヤーやストリーミングレスポンスの制限を実務レベルで理解したい方
まず前回のおさらい
前回の記事のポイントを簡単に振り返ります。
リクエスト
↓
Lambda関数の実行環境を用意(コールドスタートが起きることがある)
↓
ハンドラー関数を実行(最大15分)
↓
メモリ128MB〜10GBの範囲で処理
↓
必要なら/tmpに一時ファイル(最大10GB)
↓
レスポンスを返す(同期なら最大6MB)
- 実行時間・メモリ・一時ストレージ・デプロイパッケージサイズには、それぞれ明確な上限がある
- ファイルサイズそのものに絶対的な上限があるわけではなく、メモリ・/tmp・実行時間の組み合わせで実質的な上限が決まる
- 同時実行数はデフォルトで1,000(アカウント×リージョン単位)
前回は、この「同時実行数1,000」という数字と、「コールドスタート」という言葉について、あまり深く触れていませんでした。今回はこの2つを中心に、もう少し具体的に調べてみます。
同時実行数の仕組みをもう少し深掘りしてみた
まず、「同時実行数1,000」が何を指しているのか、公式ドキュメントを確認しながら整理してみました。
アカウント単位の総枠と関数ごとの予約
Lambdaの同時実行数は、基本的に「アカウント × リージョン」単位で管理される総枠(デフォルト1,000)を、複数の関数で分け合う形になっています。
リージョンごとの同時実行数の総枠(デフォルト1,000)
↓
┌───────────────┬───────────────┬───────────────┐
│ 関数A(予約なし) │ 関数B(予約200) │ 関数C(予約なし) │
└───────────────┴───────────────┴───────────────┘
ここで登場するのが「予約済み同時実行数(Reserved Concurrency)」です。特定の関数にだけ同時実行数の枠を予約しておくことで、
- その関数は、予約した分だけは他の関数に食われずに必ず実行できる
- 逆に、その関数はどれだけアクセスが増えても予約した数以上には同時実行されない(上限にもなる)
という2つの効果があります。例えば決済処理のように「絶対に枠を確保したいが、暴走もさせたくない」関数には、予約済み同時実行数を設定しておくと安心です。設定はAWS CLIから次のように行えます。
aws lambda put-function-concurrency \
--function-name my-payment-function \
--reserved-concurrent-executions 50
一方で、関数ごとに予約をしすぎると、他の関数が使える同時実行数が減ってしまう点には注意が必要です。予約していない関数同士は、残った枠を奪い合う形になります。
バーストコンカレンシーとスロットリング
アクセスが急増したとき、Lambdaは無制限に一気にスケールするわけではなく、「バーストコンカレンシー」と呼ばれる初期バーストの上限があり、そこを超えると1分あたり500インスタンスずつ増加していく、という段階的なスケーリングモデルを採用しています(リージョンによって初期バーストの値は異なります)。
アクセス急増
↓
初期バースト分は即座にスケール
↓
それ以上は 1分あたり+500インスタンスのペースで緩やかにスケール
↓
同時実行数の上限に到達するとスロットリング(429 TooManyRequestsException)
前回の記事では「アクセスが急増するとThrottlingが発生することがある」と書きましたが、実際にはこのように「即座に無限にスケールするわけではない」という段階的な仕組みがあることが分かりました。急激なトラフィック増加が予想される場合は、事前にAWSサポートへ同時実行数の上限緩和を申請しておく、もしくはProvisioned Concurrencyを併用しておく、といった対策が有効なようです。
コールドスタートの正体を調べてみた
「コールドスタート」という言葉はよく聞きますが、内部で何が起きているのかを整理すると、次のような流れになっています。
1. 実行環境(マイクロVM)の起動
↓
2. ランタイム(Node.js/Python/Javaなど)の起動
↓
3. ハンドラーが書かれているファイルの読み込み・グローバルスコープの初期化コード実行
↓
4. ハンドラー関数の実行(ここから「Invoke」の時間として計測される)
このうち1〜3が「初期化フェーズ」と呼ばれる部分で、ここにかかる時間がいわゆる「コールドスタートの遅延」の正体です。一度実行環境が立ち上がったあとは、同じ実行環境が一定時間(明確な保証時間は公開されていませんが、体感的には数分〜十数分程度)再利用されるため、2回目以降の呼び出し(ウォームスタート)ではこの初期化フェーズがスキップされ、ハンドラー関数の実行だけで済みます。
調べてみると、初期化フェーズの長さはランタイムや実装によってかなり差があることが分かりました。
| 要因 | コールドスタートへの影響 |
|---|---|
| ランタイムの種類 | Node.js/Pythonは比較的軽量。JavaやNETはJVM起動などの分、重くなりやすい |
| デプロイパッケージのサイズ | 大きいほどダウンロード・展開に時間がかかる |
| グローバルスコープの処理量 | DB接続の確立、大きなライブラリのimportなどが多いほど遅くなる |
| メモリ設定 | メモリを増やすとCPU性能も上がるため、初期化も速くなる傾向がある |
| VPC接続の有無 | VPC内のリソースにアクセスする関数はENI周りの準備が必要になる場合がある |
対策としてよく紹介されているのが、次のような工夫です。
- グローバルスコープでDB接続やSDKクライアントを初期化し、ハンドラー内で使い回す(毎回接続し直さない)
- 依存ライブラリを必要最小限に絞り、デプロイパッケージを小さくする
- メモリを適切に増やす(結果的にコストが下がるケースもある)
- 頻繁に呼ばれるAPIのバックエンドなど、レイテンシがシビアな関数にはProvisioned ConcurrencyやSnapStartを使う
# グローバルスコープで初期化しておくことで、
# ウォームスタート時にはこの処理がスキップされる
import boto3
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("my-table")
def lambda_handler(event, context):
response = table.get_item(Key={"id": event["id"]})
return response.get("Item")
SnapStartとProvisioned Concurrencyの違いを調べてみた
コールドスタート対策として、以前からある「Provisioned Concurrency」と、比較的新しい「SnapStart」の違いが気になったので、それぞれ調べてみました。
Provisioned Concurrency
あらかじめ指定した数の実行環境を事前に初期化した状態で待機させておく機能です。常に温まった実行環境が用意されているため、コールドスタートがほぼ発生しません。ただし、待機させている間も課金が発生するため、アクセスが少ない時間帯でも一定のコストがかかり続けます。
SnapStart
関数を発行(Publish)したタイミングで、初期化済みの実行環境のメモリとディスクの状態をスナップショットとして暗号化・キャッシュしておき、実際の呼び出し時にはそのスナップショットから実行環境を復元する、という仕組みです。事前にインスタンスを待機させ続けるわけではないため、実行時間分の課金だけで済み、追加コストなしでコールドスタートを大幅に短縮できます。
もともとJavaのLambda関数向けに提供されていた機能でしたが、2024年11月にPython(3.12以降)と.NET(8以降)でも一般提供が開始され、2026年7月にはコンテナイメージ形式の関数でもSnapStartがサポートされるようになりました。対応言語・対応形式は今後も広がっていく可能性がありそうです。
| 項目 | Provisioned Concurrency | SnapStart |
|---|---|---|
| 仕組み | 実行環境を事前に起動して待機させる | 初期化済み環境のスナップショットを復元する |
| 追加コスト | 待機時間分の課金がかかる | 追加コストなし(実行時間分のみ) |
| 対応言語 | 全ランタイム | Java、Python(3.12〜)、.NET(8〜)、コンテナイメージ |
| コールドスタート | ほぼ発生しない | 大幅に短縮されるが、ゼロではない |
| 向いているケース | レイテンシがシビアで予算も確保できる場合 | コストを抑えつつコールドスタートを改善したい場合 |
SnapStartを使う際の注意点として、スナップショット復元後に「一意性が必要な値(乱数やUUID、接続済みのソケットなど)」がスナップショット取得時点のまま使い回されてしまう可能性があるため、そうした値はハンドラー内で都度生成し直す必要がある、という点も紹介されていました。実装によっては挙動に注意が必要そうです。
レイヤーとデプロイパッケージの制限をもう少し深掘りしてみた
前回、「レイヤー数は最大5個」「ZIPは50MB、展開後250MB」という制限を紹介しましたが、実務ではこれだけでは足りないケースもあります。調べてみると、次のような回避策が一般的なようです。
ZIPデプロイ(展開後250MBまで)
↓ ライブラリが多くて収まらない
コンテナイメージ形式でデプロイ(最大10GBまで)
機械学習ライブラリなど依存パッケージが大きい場合は、ZIP+レイヤーの組み合わせではなく、最初からLambdaのコンテナイメージ対応(最大10GB)を使う設計にする方が管理しやすい、という意見も多く見かけました。また、レイヤーは共通ライブラリを複数の関数で使い回す用途にも使われますが、「1関数につき5個まで」という制限があるため、共通処理をまとめる際にはレイヤーの構成をある程度計画的に設計する必要がありそうです。
ストリーミングレスポンスの制限が更新されていた話
前回の記事執筆時点では、Lambda関数URLのレスポンスストリーミングは最大20MBという制限でしたが、調べ直したところ2025年7月に、この上限がデフォルトで200MBまで引き上げられていることが分かりました。これは全リージョン共通のデフォルト値とのことです。
Client
↓
Lambda Function URL(レスポンスストリーミング有効)
↓
Lambda(ハンドラーがストリームとして少しずつレスポンスを返す)
↓
Client(レスポンスを受け取りながら順次表示)
LLMのAPIレスポンスをストリーミングでフロントに返す、といった最近よくあるユースケースでは、この上限緩和は地味に嬉しいポイントだと感じました。ただし、同期呼び出し(Invoke API経由)のレスポンスは引き続き6MBが上限のままなので、「ストリーミングを使うかどうか」で扱えるレスポンスサイズが大きく変わる点は押さえておきたいところです。
実務での注意点
ここまで調べてみて、実際にLambdaを設計・運用する際に気をつけたほうがよさそうな点をまとめておきます。
- 予約済み同時実行数の配分:重要な関数に予約を設定しすぎると、他の関数の同時実行数を圧迫してしまうため、アカウント全体で同時実行数の配分を設計しておく必要がある
- VPC接続とコールドスタート:VPC内のリソース(RDSなど)にアクセスする関数は、ENIの準備などが必要になる場合があるため、必要のない関数まで安易にVPCへ接続しない
- DLQ(デッドレターキュー)の設定:非同期呼び出しで処理に失敗したイベントを取りこぼさないよう、DLQやLambdaの再試行設定を確認しておく
- タイムアウトの設計:15分という上限があるからといって長めに設定するのではなく、実際の処理時間に応じて適切なタイムアウトを設定し、想定外の長時間実行やコストの増加を防ぐ
- 可観測性:同時実行数やスロットリングの発生状況は、CloudWatchのメトリクス(ConcurrentExecutions、Throttlesなど)で継続的に監視しておく
個人的に調べてみて思ったこと
前回は「Lambdaにはこういう制限がある」という一覧を紹介するところまででしたが、今回もう一歩踏み込んでみると、
- 同時実行数の1,000という数字は、単なる上限ではなく「アカウント全体で分け合う総枠」であり、予約済み同時実行数によって関数間の配分をコントロールできること
- コールドスタートは「実行環境の起動→ランタイム起動→初期化コード実行」という複数のステップの合計であり、グローバルスコープの書き方次第で体感速度がかなり変わること
- SnapStartは「事前に温めておく」Provisioned Concurrencyとは発想が異なり、「スナップショットを復元する」ことで追加コストなしにコールドスタートを改善する仕組みであること
- ストリーミングレスポンスの上限のように、Lambdaの制限値自体が継続的にアップデートされていくこと
が分かりました。特に、Provisioned ConcurrencyとSnapStartを「コスト構造が根本的に違う、別のアプローチ」として理解できたのは大きな収穫でした。「コールドスタートが気になったら、とりあえずProvisioned Concurrency」という単純な話ではなく、コストと言語対応を踏まえて選ぶ必要があるのだと実感しました。
まとめ
今回は、AWS Lambdaの制限について、前回の記事よりもさらに深掘りしてみました。重要なポイントをまとめると、
- 同時実行数は「アカウント×リージョン単位の総枠」を関数間で分け合う仕組みで、予約済み同時実行数で配分をコントロールできる
- アクセス急増時は、初期バースト後は1分あたり+500インスタンスのペースで段階的にスケールする
- コールドスタートの正体は「実行環境起動+ランタイム起動+初期化コード実行」の合計時間で、グローバルスコープの書き方で体感速度が変わる
- SnapStartは追加コストなしでコールドスタートを改善できる仕組みで、Java・Python(3.12〜)・.NET(8〜)・コンテナイメージへと対応が広がっている
- ストリーミングレスポンスの上限は2025年7月に20MBから200MBへ引き上げられている
前回は「制限値そのもの」を理解するところまででしたが、今回はその制限がなぜ存在し、どう回避・緩和していくかという、より実務に近い視点まで掘り下げることができました。まだ実際に自分の手でSnapStartを有効化してコールドスタートの時間を計測したり、同時実行数の負荷試験を行ったりはできていないので、今後は手を動かしながら検証していきたいと思います。
同じようにLambdaの制限まわりに興味がある方の参考になれば幸いです。
最後まで読んでいただき、ありがとうございました!
参考
- AWS Lambda response streaming now supports 200 MB response payloads
- Understanding Lambda function scaling - AWS Lambda
- Improving startup performance with Lambda SnapStart - AWS Lambda
- AWS Lambda SnapStart for Python and .NET functions is now generally available
- AWS Lambda now supports SnapStart for container image functions