Lambda: 高負荷時の SQS リトライ挙動を検証する
はじめに
参画しているプロジェクトで、以下のような事象に遭遇しました。何が原因なのか、対策はあるのかを検証したいと思います。
遭遇した事象
- 想定の流量を大きく超える大量データを S3 にアップロードしたところ、一部データが Lambda で処理された形跡がない
- ログ上も、そもそも当該のファイルでトリガーされた記録がない
- ((( メッセージが虚空に吸い込まれた... )))
構成
- S3 の put を S3 Event Notification (S3通知) で監視し、SQS に配信
- Lambda の Event Source Mapping (ESM) で SQS をポーリングし、処理

検証すること
- 消えたメッセージはどこへ行ったのか (虚空? DLQ?)
- 消える条件は何か
- 回避の方法はあるか
作成するリソース
まずは検証環境で再現できるかの確認をやります。以下のように環境を用意しました。
※連携がうまく動くように、ポリシーは適宜追加が必要です。
| リソース | 備考 |
|---|---|
| Lambda | Node 24.x |
| S3 | |
| SQS | S3からの通知を受け取りLambdaを起動させる |
| SQS | DLQとして |
| S3通知 | SQS > many-request-test に向ける |
| ESM | SQS > many-request-test に向ける |
また、重要そうなパラメータは以下の通り設定しました。
| パラメータ名 | 対象リソース | デフォルト | 設定値 | 説明 |
|---|---|---|---|---|
| 予約された同時実行数 | Lambda | - | 5 | Lambdaが起動できる最大数 |
| タイムアウト | Lambda | 3000ms | 8000ms | Lambda タイムアウト時間 |
| Batch size | ESM | 10 | 1 | 1回のLambda呼び出しに含めるメッセージ数 |
| Maximum concurrency | ESM | - | 5 | Lambdaの並列呼び出し最大数 |
| 可視性タイムアウト | SQS | 30s | 10s | 今回はコンシューマが1つなのでリトライ間隔として考える |
Lambda
頭書の事象が発生した環境が Node でしたので、合わせて Node にしました。起動したか・正しく終了したか・どのファイルがトリガーで起動したか のログを出すようにします。また、実行時間が短すぎると高負荷の再現でたくさんのテストファイルが必要になるので、5000ms 待機させてます。
export const handler = async (event) => {
const body = JSON.parse(event.Records[0].body);
const s3Path = body.Records[0].s3;
const bucket = s3Path.bucket.name;
const key = decodeURIComponent(s3Path.object.key.replace(/\+/g, ' '));
const filename = key.split('/').pop();
console.log("起動したよ", JSON.stringify({ bucket, key, filename }));
await new Promise(resolve => setTimeout(resolve, 5000));
console.log("終了したよ", JSON.stringify({ bucket, key, filename }));
};
検証方針
実際に S3 にファイルを投入して検証します。ファイルそのものは空ファイルです。各メトリクスを取得し、何が起きてるかみます。
起動数・終了数のチェック
CloudWatch Logs Insights で以下クエリを投げます。
filter @type = "REPORT" or @message like /起動したよ/ or @message like /終了したよ/
| stats
count(@type = "REPORT") as start_counts,
count(@initDuration != "" and @type = "REPORT") as cold_start_counts,
sum(@message like /起動したよ/) as started_log_counts,
sum(@message like /終了したよ/) as finished_log_counts
これで、以下の表のようなメトリクスが取れます。
| メトリクス名 | 内容 |
|---|---|
| start_counts | 起動の総数 |
| cold_start_counts | コールドスタートの総数 |
| started_log_counts | 起動ログの件数 |
| finished_log_counts | 終了ログの件数 |
処理したファイルのチェック
CloudWatch Logs Insights で以下クエリを投げます。
filter @message like /起動したよ/
| stats count() by filename
これで、処理されたファイル名が全件取得できますので、欠損があればわかります。
DLQ の確認
用意した DLQ にメッセージが入ってくるか確認します。件数が、投入したファイル数 - 処理されたファイル数 と同じであれば、虚空に消えたファイルがいないことがいえそうです。
検証
200ファイルで検証
| start_counts | cold_start_counts | started_log_counts | finished_log_counts |
|---|---|---|---|
| 200 | 5 | 200 | 200 |
綺麗に終わりました。ファイル名も欠損なく、DLQ も空です。まだ捌けてそうなので投入するファイルを増やします。
300 ファイルで検証
| start_counts | cold_start_counts | started_log_counts | finished_log_counts |
|---|---|---|---|
| 297 | 3 | 297 | 297 |
欠損してますね。処理されたファイル名一覧を見ると、実際に3ファイル処理されていません。DLQ のメッセージも、以下のように3件取得でき、内容も未処理のファイルと一致しました。

一旦考える
以下のように、スロットルのカウントが上がってるので、途中からは「ポーリングしたけどスロットルで拒否される」を繰り返している気がします。

続いて、ESM の最大同時実行数と、Lambda の最大同時実行数の設定に違いがあるか、確認してみます。
300 ファイルで検証 ~Lambda の同時実行数を絞らない版~
| start_counts | cold_start_counts | started_log_counts | finished_log_counts |
|---|---|---|---|
| 300 | 5 | 300 | 300 |
通りました。逆もやってみます。
300 ファイルで検証 ~ESM の同時実行数を絞らない版~
| start_counts | cold_start_counts | started_log_counts | finished_log_counts |
|---|---|---|---|
| 281 | 2 | 281 | 281 |
だめでした。捌けているリクエストが、どちらも制限している時より減っているのは、Lambdaの上限のみだと ESM の制限がかからず、Lambda, ESM 両方の同時実行数を制限した時よりスロットルにかかるリクエストが増えたためだと考えられます。実際に、スロットルの件数は以下のように差異がありました。

(左が両方制限、右がLambdaのみ制限)
結論のようなもの
原因
- Lambda の予約された同時実行で設定した並列数で捌ききれない量のリクエストを投げると、Lambda はスロットルを返す。
- スロットルで拒否されたデータは SQS に戻り、ReceiveCount を増やす
- リトライ数上限まで拒否され続けたタスクは、そのまま DLQ へ飛ばされる
対策
- 基本的には Lambda の予約された同時実行ではなく、ESM の Maximum concurrency で絞る
- AWSのドキュメント での記載には、Lambdaの設定値はESMの設定値以上にすべし、とありますが、イコールだと負荷状況によっては破綻しうるようです。
最大同時実行数は、1 つのキューが関数の予約された同時実行のすべてを使用したり、アカウントの同時実行クォータの残りのすべてを使用したりしないようにするために使用できます。
ーーー
最大同時実行数を設定する場合は、関数の予約された同時実行数が、関数にマップされたすべての Amazon SQS イベントソースの合計最大同時実行数以上になるようにしてください。合計数未満になった場合は、Lambda がメッセージをスロットルする可能性があります。
簡単にまとめ
- Lambda の予約された同時実行を 「この並列数で実行してほしい」 の上限として使うのは危険で、DLQへの溢れが発生しうる。
- 特定の並列数までベストエフォートでやって欲しいなら、ESM の Maximum concurrency で制限をかけて、Lambda 側は設定しない or それより余裕を持って設定する方がいい。
- 公式ドキュメントには、関数の予約された同時実行数 >= Maximum concurrency と記載されているが、同じ設定値はストレスがかかったときに破綻するので余裕を持たせたほうが良さそう。
最後に
あくまで単純化した上での結果なので、プロジェクトで僕が直面している課題は他にも問題が隠れているのかもしれないですが、一因であることは間違いないです。結果としてほぼAWS公式ドキュメントに書いてある内容になってしまいましたが、理解が深まってよかったと思います。






