SQSの重複メッセージとReceiptHandleIsInvalidExceptionの解決方法

intermediate☁️ AWS2026-04-12| AWS SQS、Java AWS SDK (v1/v2)、Boto3 (Python)、Node.js AWS SDK、分散コンシューマー環境。

Error Message

com.amazonaws.services.sqs.model.ReceiptHandleIsInvalidException: The input receipt handle is not a valid receipt handle (Service: AmazonSQS; Status Code: 404)
#aws-sqs#可視性タイムアウト#分散システム#cloudwatch#java

要約:手っ取り早い解決策

このエラーは、コンシューマーがメッセージを処理するのに、キューに設定された VisibilityTimeout 以上の時間がかかった場合に発生します。SQSはワーカーがクラッシュしたと判断し、そのメッセージを他のコンシューマーから見える状態に戻します。その結果、元の ReceiptHandle は無効化されます。

解決策: VisibilityTimeout を最大処理時間の少なくとも1.5倍に引き上げてください。処理時間が予測できないジョブの場合は、ChangeMessageVisibility APIを使用して「ハートビート」を実装し、ワーカーがアクティブな間はハンドルを維持するようにします。

発生原因:時間との戦い

コンシューマーがメッセージを取得した際、SQSはすぐにメッセージを削除するのではなく、特定の期間だけ非表示にします。あなたの役割は、その期間が終了する前に処理を完了し、DeleteMessage を呼び出すことです。処理が遅すぎると、メッセージは他のコンシューマーが取得できるように「再出現」します。

ReceiptHandleIsInvalidException は、以下のようなタイムラインの結果として発生します:

  • T+0: コンシューマーAがメッセージを取得します。キューの VisibilityTimeout は30秒に設定されています。
  • T+31: コンシューマーAは、50MBの画像のリサイズなど、重いタスクをまだ処理中です。SQSはコンシューマーAが停止したと判断し、メッセージをキューに戻します。
  • T+32: コンシューマーBが同じメッセージを取得します。SQSは新しい ReceiptHandle を発行します。古いハンドルは破棄されます。
  • T+40: コンシューマーAがようやく処理を終え、メッセージを削除しようとします。
  • 結果: SQSは404を返します。より新しいハンドルが存在するか、可視性ウィンドウが期限切れになったため、ハンドルは無効です。

効果的な解決策

1. グローバルな Visibility Timeout を調整する

ロジックの実行に常に2分かかるにもかかわらず、タイムアウトが30秒しか設定されていない場合、設定が実態に合っていません。成功する可能性のある最も遅いリクエストに合わせて、キューの設定を調整してください。

AWS CLI を使用する場合:

aws sqs set-queue-attributes \
    --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/my-queue \
    --attributes VisibilityTimeout=300

Terraform を使用する場合:

resource "aws_sqs_queue" "my_queue" {
  name                       = "data-processor-queue"
  visibility_timeout_seconds = 300 # 5 minutes
}

2. プログラムによるハートビートの実装

ジョブに10秒かかるか10分かかるか予測できない場合があります。グローバルに15分という長いタイムアウトを設定するのは危険です。ワーカーが実際にクラッシュした場合、そのメッセージは15分間見えないままになり、パイプラインが停滞してしまいます。代わりに、タイムアウトを動的に延長します。

Java (AWS SDK v2) の例:

// 長時間実行されるタスクの間、30秒ごとに呼び出します
SqsClient sqsClient = SqsClient.builder().build();

ChangeMessageVisibilityRequest request = ChangeMessageVisibilityRequest.builder()
    .queueUrl(queueUrl)
    .receiptHandle(receiptHandle)
    .visibilityTimeout(60) // 期限をさらに60秒延長します
    .build();

sqsClient.changeMessageVisibility(request);

Python (Boto3) の例:

import boto3
sqs = boto3.client('sqs')

def extend_lifetime(receipt_handle):
    # SQSに時間が必要であることを伝えます
    sqs.change_message_visibility(
        QueueUrl='YOUR_QUEUE_URL',
        ReceiptHandle=receipt_handle,
        VisibilityTimeout=60
    )

3. 冪等性(べきとうせい)を考慮した設計

分散システムでは、メッセージが2回処理される可能性があることを想定しなければなりません。タイムアウトの設定が完璧であっても、ネットワークの瞬断などは起こり得ます。重い処理を行う前に、データベースの更新に一意のキーを使用するか、ステータスフラグ(例:WHERE status != 'COMPLETED')を確認するようにしてください。

修正の検証方法

Amazon CloudWatch ダッシュボードで以下のシグナルを確認してください:

  • ApproximateNumberOfMessagesVisible: この値はゼロに向かうはずです。コンシューマーがアクティブなのに高い値のままであれば、メッセージがタイムアウトして再利用されている可能性があります。
  • NumberOfMessagesReceived vs. Deleted: 正常なシステムでは、これらはほぼ1:1になるはずです。受信数が5,000なのに削除数が3,000しかない場合は、重大な重複問題が発生しています。
  • ログパターン: CloudWatch Logs Insights のクエリを設定して、ReceiptHandleIsInvalidException の発生回数をカウントします。修正後は、このカウントがゼロになるはずです。

よくある落とし穴

  • Lambda の「6倍ルール」: SQSが Lambda をトリガーする場合、SQSの VisibilityTimeout は Lambda の Timeout の少なくとも6倍に設定してください。これにより、前の実行がまだ処理中である間に Lambda サービスが同じイベントを再試行するのを防げます。
  • バッチ処理のリスク: 一度に10件のメッセージを取得すると、それらすべてに対して即座にタイムアウトのカウントが始まります。もしメッセージ1件目の処理に29秒かかり、タイムアウト設定が30秒であれば、2件目から10件目のメッセージは処理を開始する前に期限切れになる可能性が非常に高くなります。

Related Error Notes