SQS→Lambda、10件中1件失敗で10件全部再処理されてた

  • AWS
  • SQS
  • Lambda

この記事は、構成と文章の整理にAIを使っています。
あくまで読みやすくするための補助で、内容そのものをAIにすべて任せて書いているわけではありませんが、ところどころ相談しながら整えています。

SQSをイベントソースにしたLambdaで、エラーが起きたときの挙動を確認した。

何が起きたか

現状の構成はこんな感じ。

const queue = new sqs.Queue(this, 'ExampleQueue', {
visibilityTimeout: Duration.seconds(30),
retentionPeriod: Duration.days(7),
});
events: [
new sources.SqsEventSource(queue, {
batchSize: 10,
maxBatchingWindow: Duration.seconds(10),
enabled: true,
}),
],

DLQなし、reportBatchItemFailuresなし。この状態だと、10件バッチで受け取って9件成功・1件失敗しても、Lambdaが例外で落ちれば10件全部がキューに戻ってやり直しになる。DLQを設定していなければ、SQSの保持期間(最大14日)までこれが繰り返される。

reportBatchItemFailuresを足すとどうなるか

{
"batchItemFailures": [
{ "itemIdentifier": "messageId" }
]
}

Lambdaがこれを返すようにすると、失敗したmessageIdだけが再試行対象になり、成功した9件はその場で削除される。

ここで気になったのが、「reportBatchItemFailures: trueだけ設定して、Lambda側の実装(batchItemFailuresを返す処理)をまだ書いていなかったらどうなるか」という点。これは特に問題なくて、設定を有効にしていてもLambdaがbatchItemFailuresを返さなければ、今まで通り「成功なら全部削除、失敗なら全部再試行」の挙動のまま。Lambda側を実装した時点で部分成功が効くようになる。設定と実装は別々に進めて大丈夫、ということ。

maxReceiveCountの数え方

DLQへのRedrivePolicyでmaxReceiveCount: 5と設定した場合、

ReceiveCount=1 → 失敗
ReceiveCount=2 → 失敗
...
ReceiveCount=6 → DLQへ移動

という流れになる。つまり「5回まで試して、6回目の受信でDLQ行き」。初回を1回目としてカウントするので、設定値=リトライ回数ではなく「初回+リトライ回数」がmaxReceiveCountになる。

バリデーションエラーは別扱いにした

DLQは「技術的な失敗(DynamoDB障害、S3障害、タイムアウトなど)」を想定した仕組みで、何回リトライしても直らない「決定的なエラー(スキーマ違反、必須項目不足、型違いなど)」を混ぜるとDLQの中身がノイズだらけになる。

なので、バリデーションエラーはInvalidQueueのような別キューに直接送る設計にした。このキューを人が見るだけ・監視だけの用途なら、InvalidQueue自体にDLQは不要。逆にLambdaがInvalidQueueを読んで再整形・再投入するような処理を挟むなら、そのLambda用に別途DLQを付ける必要がある。

まとめると

  • DLQなし+reportBatchItemFailuresなし → 1件失敗で全件が保持期間いっぱいまでループする
  • reportBatchItemFailuresは設定と実装がセットで初めて効く。設定だけなら挙動は変わらない
  • maxReceiveCountは「初回含めてN回目でDLQ」というカウント
  • 技術的失敗はDLQ、決定的なバリデーションエラーは別キューに分ける方が運用しやすい