
SQS のデッドレターキューの中身を、受信の回数を増やさずに確認できるか試してみた
はじめに
テクニカルサポートの 片方 です。
非同期の処理を Amazon SQS で組むと、処理に失敗したメッセージはデッドレターキュー(DLQ)にたまります。DLQ は、何度受信しても削除されなかったメッセージを移しておく別のキューです。障害を調べるときは、まず DLQ の中身を読んで、どの注文や会員の処理が落ちたかを確認します。
ただ DLQ の中身を確かめるために受信しただけで、メッセージに影響が出ないか気になりました。
SQS のメッセージは受信するたびに受信の回数(ApproximateReceiveCount)が増え、この回数で DLQ に移るかどうかが決まります。DLQ を調べるたびに回数が増えると、後で元のキューに戻して再処理するときに、すぐまた DLQ に落ちるのではないかと考えました。
私が最初に考えたのは、可視性タイムアウトを 0 にして受信すれば、ほかの受信者に影響せずに中身を読めるという方法です。東京リージョンに作ったキューと DLQ で、この方法で受信の回数が増えないのか、増えるなら再処理に響くのかを確かめました。障害のときの DLQ の確認と再処理の手順を決めておく場面を想定しています。
構成

元のキュー by-y12-src(標準キュー)に再処理ポリシーを付け、最大受信数(maxReceiveCount)を 2 にしました。最大受信数は、受信が何回を超えたら DLQ に移すかの値です。DLQ の by-y12-dlq には再処理ポリシーを付けていません。
元のキューに架空の注文のメッセージを 3 件送り、可視性タイムアウト 0 で受信を繰り返して DLQ に移しました。可視性タイムアウトは、受信したメッセージをほかの受信者から隠しておく秒数です。
DLQ は 3 つの方法で読みました。件数だけを返す GetQueueAttributes、可視性タイムアウト 0 の ReceiveMessage、確認用のスクリプト(peek_dlq.py)です。最後に StartMessageMoveTask で元のキューに戻し、受信の回数がどうなるかを見ました。
先に結論から
- 標準キューで、ReceiveMessage を使って回数を増やさずに本文を読む方法はありませんでした。可視性タイムアウト 0 の受信では、今回は 1 回の呼び出しで同じメッセージが 10 回(MaxNumberOfMessages の上限まで)返り、回数が 3 から 12 まで急激に増えました。
- DLQ での回数は、元のキューで数えた回数の続きでした。DLQ に再処理ポリシーが無ければ、回数が増えても DLQ からは移りません。
- StartMessageMoveTask で元のキューに戻したメッセージは、受信の回数が 1 から数え直されました。DLQ で見た 2 件の MessageId は、戻した後の 3 件のどれとも一致しませんでした。
- 可視性タイムアウトを 60 秒にして 1 回ずつ受信し、すぐ 0 に戻す形にすると、1 回の確認で増える回数は 1 でした。
やってみた
1. 元のキューと DLQ を作る
DLQ を先に作り、その ARN を元のキューの再処理ポリシーに入れます。
aws sqs create-queue --queue-name by-y12-dlq --attributes MessageRetentionPeriod=86400 --tags blog-yattemita=Y12
aws sqs create-queue --queue-name by-y12-src --attributes file://src-attrs.json --tags blog-yattemita=Y12
src-attrs.json は次の内容です(ARN の番号はマスクしています)。
{"MessageRetentionPeriod": "86400", "VisibilityTimeout": "30", "RedrivePolicy": "{\"deadLetterTargetArn\": \"arn:aws:sqs:ap-northeast-1:<account>:by-y12-dlq\", \"maxReceiveCount\": \"2\"}"}
検証なので、保持期間はどちらも 1 日にしました。AWS は、DLQ の保持期間を元のキューより長くすることをベストプラクティスとしています(Amazon SQS でのデッドレターキューの使用)。標準キューでは、メッセージの期限は元のキューに入った時刻で数えるためです。
2. メッセージを DLQ に移す
元のキューに order-0001 から order-0003 の 3 件を同じ形で送り、可視性タイムアウト 0 で受信を 3 回繰り返しました。$SRC と $DLQ は、get-queue-url で取ったキューの URL です。
aws sqs send-message --queue-url "$SRC" --message-body '{"orderId":"order-0001","action":"charge"}'
aws sqs receive-message --queue-url "$SRC" --max-number-of-messages 10 --visibility-timeout 0 --wait-time-seconds 2 \
--message-system-attribute-names ApproximateReceiveCount \
--query 'Messages[].[Body,Attributes.ApproximateReceiveCount]' --output text
1 回目と 2 回目の結果は次のとおりです。
{"orderId":"order-0003","action":"charge"} 1
{"orderId":"order-0003","action":"charge"} 2
{"orderId":"order-0001","action":"charge"} 1
{"orderId":"order-0001","action":"charge"} 2
{"orderId":"order-0002","action":"charge"} 1
{"orderId":"order-0002","action":"charge"} 2
1 回の呼び出しで、同じメッセージが 2 回返っています。3 回目の呼び出しは何も返さず、GetQueueAttributes で見ると、DLQ が 3 件、元のキューが 0 件になっていました。
"ApproximateNumberOfMessages": "3",
"ApproximateNumberOfMessagesNotVisible": "0",
確認してみた
可視性タイムアウト 0 で DLQ を読む
DLQ にも同じ受信を 3 回実行しました。DLQ に移ったメッセージには、元のキューの ARN(DeadLetterQueueSourceArn)も付くので、一緒に表示しています。
aws sqs receive-message --queue-url "$DLQ" --max-number-of-messages 10 --visibility-timeout 0 --wait-time-seconds 2 \
--message-system-attribute-names All \
--query 'Messages[].[Body,Attributes.ApproximateReceiveCount,Attributes.DeadLetterQueueSourceArn]' --output text
1 回目の結果の頭と終わりです。
{"orderId":"order-0003","action":"charge"} 3 arn:aws:sqs:ap-northeast-1:<account>:by-y12-src
{"orderId":"order-0003","action":"charge"} 4 arn:aws:sqs:ap-northeast-1:<account>:by-y12-src
(中略。同じメッセージが 5 から 11 まで続く)
{"orderId":"order-0003","action":"charge"} 12 arn:aws:sqs:ap-northeast-1:<account>:by-y12-src
1 回の呼び出しで、同じ 1 件が 10 回返りました。1 回に返す件数の上限(MaxNumberOfMessages の 10 件)の枠を、同じメッセージが埋めています。
2 回目の呼び出しは order-0002 が 3 から 12 まで、3 回目は order-0003 がまた 13 から 22 まででした。3 回読んだだけで、受信の回数は合わせて 30 増えています。order-0001 は一度も返りませんでした。
待ち時間を付けない短いポーリングで撮り直したときは、3 回でした。今回の検証では、返る回数は呼び出しによって違いました。

DLQ での回数が 3 から始まっているのは、元のキューで 2 回受信した続きだからです。
ApproximateReceiveCount – Returns the number of times a message has been received across all queues but not deleted.
可視性タイムアウト 0 の受信は、ほかの受信者からメッセージを隠さないだけで、受信の回数は数えられます。ReceiveMessage で本文を読む限り、回数を増やさずに読む方法はありませんでした。
再処理すると回数はどうなるか
回数が 22 になっても、メッセージは DLQ に残っていました。問題は元のキューに戻したときです。最大受信数が 2 のキューに、回数 22 のまま戻ると、最初の受信でまた DLQ に落ちると考えました。
送り先を指定しない StartMessageMoveTask は、メッセージを元のキューに戻します。
aws sqs start-message-move-task --source-arn arn:aws:sqs:ap-northeast-1:<account>:by-y12-dlq
aws sqs list-message-move-tasks --source-arn arn:aws:sqs:ap-northeast-1:<account>:by-y12-dlq \
--query 'Results[0].[Status,ApproximateNumberOfMessagesMoved,ApproximateNumberOfMessagesToMove]' --output text
COMPLETED 3 3
完了の直後に DLQ の件数を見ると、まだ 3 件と出ました。1 分ほど置くと 0 件になり、元のキューが 3 件になりました。今回は、件数が遅れて変わりました。
元のキューで 1 件を受信すると、受信の回数は 1 でした。
{"orderId":"order-0002","action":"charge"} 1
DLQ で受信した回数は引き継がれていません。
次に、もう一度 DLQ に落としてから、再処理の前に DLQ を受信し、返った MessageId をスクリプトの中で控えました(ID は表示せず、件数だけを比べています)。このときの受信で返ったのは、3 件のうち 2 件でした。再処理の後に元のキューの 3 件と比べると、一致した ID は 0 件でした。
再処理タスクにより、保持期間がリセットされます。すべての再処理されたメッセージは、新しい messageID を持つ新しいメッセージと見なされ、再処理されたメッセージに enqueueTime が割り当てられます。
コンソールの[デッドレターキューの再処理タスク]タブにも、CLI で実行した再処理が「正常に完了しました」と出ていました。画像は、同じ構成(by-12-src と by-12-dlq)を作り直し、AWS CLI で再処理を実行して撮ったものです。

Lambda 関数の非同期呼び出しの DLQ のように、送信元が SQS でない DLQ は、StartMessageMoveTask の対象外です(2026 年 10 月時点)。API リファレンスでは、現在はサポートされていないとされています。
1 回ずつ読んで、すぐ戻す
受信の回数の増え方を小さくする、確認用のスクリプト(peek_dlq.py)を書きました。可視性タイムアウトを 60 秒にして受信し、その間に全部を集めてから、ChangeMessageVisibilityBatch で 0 に戻します。
60 秒の間は、同じメッセージは原則として返りません。そのため、1 回の確認で受信するのは、原則として 1 件につき 1 回です。まれに重なって返っても、MessageId で 1 件にまとめます。
再処理ポリシーを持つキューを読むと、受信の回数が増え、最大受信数を超えた分は DLQ に移ってしまいます。再処理ポリシー(RedrivePolicy)を持つキューを渡したときは、受信せずに止めるようにしました。
再試行は、Boto3 の standard モードに任せています。Config の max_attempts は初回を含まない数なので、再試行は最大 5 回(初回を含めて 6 回)です(Boto3 のガイドの Retries)。
メッセージは削除せず、可視性を戻すだけなので、キューの中身は変わりません。ただし、実行するたびに受信の回数は原則 1 増えます。
import argparse
import json
import sys
import boto3
from botocore.config import Config
CONFIG = Config(retries={"mode": "standard", "max_attempts": 5})
def peek(queue_name, hold_seconds=60, limit=100):
sqs = boto3.client("sqs", config=CONFIG)
url = sqs.get_queue_url(QueueName=queue_name)["QueueUrl"]
attrs = sqs.get_queue_attributes(QueueUrl=url, AttributeNames=["RedrivePolicy", "ApproximateNumberOfMessages"])["Attributes"]
if "RedrivePolicy" in attrs:
sys.exit(f"{queue_name} は再処理のポリシーを持つので、受信すると DLQ に移ります。覗かずに止めます。")
print(f"ApproximateNumberOfMessages: {attrs['ApproximateNumberOfMessages']}")
seen = {}
try:
while len(seen) < limit:
# 受信したメッセージは hold_seconds の間だけ見えなくなる。その間は原則として同じメッセージが返らない
res = sqs.receive_message(
QueueUrl=url,
MaxNumberOfMessages=10,
VisibilityTimeout=hold_seconds,
WaitTimeSeconds=1,
MessageSystemAttributeNames=["ApproximateReceiveCount", "SentTimestamp"],
)
msgs = res.get("Messages", [])
if not msgs:
break
for m in msgs:
seen.setdefault(m["MessageId"], m) # 同じ MessageId が二度来ても 1 件として扱う
finally:
# 見えない状態のまま残さない(可視性タイムアウトが過ぎた物は MessageNotInflight で Failed に入るが、hold_seconds 秒後には自然に見える状態に戻っている)
handles = [m["ReceiptHandle"] for m in seen.values()]
for i in range(0, len(handles), 10):
entries = [{"Id": str(n), "ReceiptHandle": h, "VisibilityTimeout": 0} for n, h in enumerate(handles[i:i + 10])]
failed = sqs.change_message_visibility_batch(QueueUrl=url, Entries=entries).get("Failed", [])
for f in failed:
print(f"見える状態に戻せなかった: {f['Code']}({hold_seconds} 秒後に自然に戻る)", file=sys.stderr)
for m in seen.values():
print(json.dumps({"body": m["Body"], "receiveCount": int(m["Attributes"]["ApproximateReceiveCount"])}, ensure_ascii=False))
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument("queue_name")
a = p.parse_args()
peek(a.queue_name)
なお、実行している 60 秒の間は、DLQ の ApproximateNumberOfMessages が少なく出ます。
元のキューを渡すと、受信せずに止まりました。
※ by-y12-src は再処理のポリシーを持つので、受信すると DLQ に移ります。覗かずに止めます。
再処理の後に、もう一度 DLQ に落とした状態で、DLQ に続けて 2 回実行しました。3 件が 1 回ずつ表示され、回数は 3 から 4 に 1 だけ増えました。実行した後の GetQueueAttributes は、見える 3 件・見えない 0 件で、可視性は戻っていました。
ApproximateNumberOfMessages: 3
{"body": "{\"orderId\":\"order-0002\",\"action\":\"charge\"}", "receiveCount": 3}
{"body": "{\"orderId\":\"order-0001\",\"action\":\"charge\"}", "receiveCount": 3}
{"body": "{\"orderId\":\"order-0003\",\"action\":\"charge\"}", "receiveCount": 3}
ApproximateNumberOfMessages: 3
{"body": "{\"orderId\":\"order-0001\",\"action\":\"charge\"}", "receiveCount": 4}
{"body": "{\"orderId\":\"order-0003\",\"action\":\"charge\"}", "receiveCount": 4}
{"body": "{\"orderId\":\"order-0002\",\"action\":\"charge\"}", "receiveCount": 4}
検証時に実行した結果では、order-0002 だけが 6 でした。直前に可視性タイムアウト 0 で受信したとき、order-0002 だけが 3 回返った分です。

まとめ
可視性タイムアウトを 0 にすれば、中身を読んでも影響しないだろうと思っていました。実際は、1 回の呼び出しで同じメッセージが 10 回返り、受信の回数が一気に増えました。ただ、心配していた再処理のほうは、StartMessageMoveTask で戻すと回数が 1 から数え直されるので、DLQ を調べた回数が再処理の妨げになることはありませんでした。
障害のときの手順にするなら、件数は GetQueueAttributes で見て、中身は peek_dlq.py で 1 件ずつ確認し、戻すのは StartMessageMoveTask に任せる形にします。自前で送り直す場合は試していないので、次は FIFO キューの DLQ と合わせて確かめてみたいと思います。
本ブログが、DLQ の調べ方に迷った誰かの参考になれば幸いです。
参考資料
- Amazon SQS でのデッドレターキューの使用 - Amazon Simple Queue Service
- Amazon SQS でデッドレターキューの再処理を設定する方法について説明します。 - Amazon Simple Queue Service
- ReceiveMessage - Amazon Simple Queue Service
- StartMessageMoveTask - Amazon Simple Queue Service
- Amazon SQS queue and message identifiers - Amazon Simple Queue Service
- Retries - Boto3 1.43.118 documentation
クラスメソッドオペレーションズ株式会社について
クラスメソッドグループのオペレーション企業です。
運用・保守開発・サポート・情シス・バックオフィスの専門チームが、IT・AIをフル活用した「しくみ」を通じて、お客様の業務代行から課題解決や高付加価値サービスまでを提供するエキスパート集団です。
当社は様々な職種でメンバーを募集しています。
「オペレーション・エクセレンス」と「らしく働く、らしく生きる」を共に実現するカルチャー・しくみ・働き方にご興味がある方は、クラスメソッドオペレーションズ株式会社 コーポレートサイト をぜひご覧ください。※2026年1月 アノテーション㈱から社名変更しました







