Amazon SQSは、メッセージをキューに送り(send-message)、受け取り(receive-message)、処理が終わったら自分で消す(delete-message)という3つの操作で使うメッセージキューです。 受信しただけではメッセージは消えず、可視性タイムアウトという時間が過ぎると、もう一度受信できる状態に戻ります。

この記事では、キューの作成から送信・受信・削除・可視性タイムアウトの変更・全消去までを、AWS CLI と boto3 の両方で並べ、応答の JSON をそのまま載せます。さらに「受信しただけでは消えない」「タイムアウト切れで再受信される」「受信回数が増えて DLQ へ移る」という動きを、経過秒数つきの実行結果で確かめます。後半は標準キューと FIFO キューの違い、MessageGroupId を省いたときのエラー文、属性の早見表です。

確認環境:Ubuntu 24.04 / AWS CLI 1.46.1 / Python 3.11.15 / boto3 1.43.103。AWS の API をローカルで再現するツール(moto 5.2.3)で実行しています。掲載コマンドは接続先の指定を外してあり、実際の AWS でそのまま使える形です。出力中のキュー URL のホスト名は、実際の AWS の形に置き換えています。

SQS操作の早見表(CLIとboto3)

やりたいこと AWS CLI boto3 client 主な応答
キューを作る aws sqs create-queue --queue-name 名前 sqs.create_queue(QueueName=) QueueUrl
キューの URL を調べる aws sqs get-queue-url --queue-name 名前 sqs.get_queue_url(QueueName=) QueueUrl
送る aws sqs send-message --queue-url URL --message-body 本文 sqs.send_message(QueueUrl=, MessageBody=) MessageId、MD5OfMessageBody
受け取る aws sqs receive-message --queue-url URL sqs.receive_message(QueueUrl=) Messages(Body、ReceiptHandle など)
消す aws sqs delete-message --queue-url URL --receipt-handle ハンドル sqs.delete_message(QueueUrl=, ReceiptHandle=) なし
見えない時間を変える aws sqs change-message-visibility ... --visibility-timeout 秒 sqs.change_message_visibility(...) なし
全部消す aws sqs purge-queue --queue-url URL sqs.purge_queue(QueueUrl=) なし
件数や設定を見る aws sqs get-queue-attributes --attribute-names All sqs.get_queue_attributes(...) Attributes
設定を変える aws sqs set-queue-attributes --attributes 名前=値 sqs.set_queue_attributes(...) なし
まとめて送る(10件まで) aws sqs send-message-batch --entries sqs.send_message_batch(Entries=) Successful、Failed

メッセージがたどる流れは次のとおりです。

sequenceDiagram
    participant P as 送る側
    participant Q as キュー
    participant C as 受け取る側
    P->>Q: send-message
    C->>Q: receive-message
    Q-->>C: Body と ReceiptHandle
    Note over Q: 可視性タイムアウトの間は他から見えない
    C->>Q: delete-message(処理が終わったら)
    Note over Q: 消さずに時間が過ぎると、また受信できる状態に戻る

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Amazon SQSの使い方|送受信・削除と可視性タイムアウト、FIFO」

キューを作る・URLを調べる

SQS の操作は、キュー名ではなくキュー URLを指定して行います。作成時の応答に入っているほか、名前から調べることもできます。

aws sqs create-queue --queue-name orders-queue
Q=$(aws sqs get-queue-url --queue-name orders-queue --query QueueUrl --output text)
echo "$Q"
{
    "QueueUrl": "https://sqs.ap-northeast-1.amazonaws.com/123456789012/orders-queue"
}
https://sqs.ap-northeast-1.amazonaws.com/123456789012/orders-queue

以降の CLI の例では、この $Q を使います。boto3 では次のように書きます。

import boto3

sqs = boto3.client("sqs")
url = sqs.create_queue(QueueName="demo-queue", Attributes={"VisibilityTimeout": "3"})["QueueUrl"]
print(url)
print(sqs.get_queue_url(QueueName="demo-queue")["QueueUrl"] == url)
https://sqs.ap-northeast-1.amazonaws.com/123456789012/demo-queue
True

Attributes の値は、数値でも文字列で渡します。ここでは後の確認を短い時間で済ませるため、可視性タイムアウトを3秒にしています(既定は30秒)。

メッセージを送る(send-message)

aws sqs send-message --queue-url "$Q" --message-body '{"order_id":"0001","item":"ノート"}'
{
    "MD5OfMessageBody": "584b39cce659cabe09b65265ee75e4c7",
    "MessageId": "63566910-0a10-43b0-bcf5-fed96174fbc5"
}

応答の MessageId は SQS が付けた ID、MD5OfMessageBody は本文の MD5 で、送った内容の照合に使えます。

本文とは別に、種類や回数などの付加情報をメッセージ属性として持たせることもできます。

aws sqs send-message --queue-url "$Q" --message-body 'second' \
  --message-attributes '{"kind":{"DataType":"String","StringValue":"order"},"retry":{"DataType":"Number","StringValue":"0"}}'
aws sqs get-queue-attributes --queue-url "$Q" \
  --attribute-names ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible
{
    "MD5OfMessageBody": "a9f0e61a137d86aa9db53465e0801612",
    "MD5OfMessageAttributes": "09857a05c2706866de09a43a8ca66997",
    "MessageId": "cd4fa904-0375-4c2d-9a72-6d6d5f66c628"
}
{
    "Attributes": {
        "ApproximateNumberOfMessages": "2",
        "ApproximateNumberOfMessagesNotVisible": "0"
    }
}

ApproximateNumberOfMessages が受信できるメッセージの数、ApproximateNumberOfMessagesNotVisible が受信済みでまだ消されていない(処理中の)数です。

boto3 では、本文は文字列で渡します。dict を送りたいときは json.dumps で文字列にします。

import json, hashlib

body = json.dumps({"order_id": "0001", "item": "ノート"}, ensure_ascii=False)
r = sqs.send_message(QueueUrl=url, MessageBody=body)
print({k: r[k] for k in ("MessageId", "MD5OfMessageBody")})
print("手元で計算した MD5:", hashlib.md5(body.encode("utf-8")).hexdigest())
{'MessageId': '512be379-574b-424e-8500-86859f36d461', 'MD5OfMessageBody': 'ace2de3fed515ebe90e26055aa380104'}
手元で計算した MD5: ace2de3fed515ebe90e26055aa380104

メッセージを受け取る(receive-message)

aws sqs receive-message --queue-url "$Q" \
  --max-number-of-messages 1 \
  --message-system-attribute-names All \
  --message-attribute-names All \
  --wait-time-seconds 1 | tee msg.json
{
    "Messages": [
        {
            "MessageId": "63566910-0a10-43b0-bcf5-fed96174fbc5",
            "ReceiptHandle": "rkuyjstoghretsmddhoenqorryxtpxvptoozbgquxhaaxspbbkkhdawsmrohvlsjyvghnaupiycellmcmhiwsfuganlolqtkfncusbojvbdclydwxzshjarkrtmuqqxfgbclnwfcoxyosntdsiahzgpdsrpuxxawktdxpslbeqkadkpvzrzwnvmha",
            "MD5OfBody": "584b39cce659cabe09b65265ee75e4c7",
            "Body": "{\"order_id\":\"0001\",\"item\":\"ノート\"}",
            "Attributes": {
                "SenderId": "AIDAIT2UOQQY3AUEKVGXU",
                "SentTimestamp": "1791021867408",
                "ApproximateReceiveCount": "1",
                "ApproximateFirstReceiveTimestamp": "1791021868542"
            }
        }
    ]
}
応答のキー 内容
MessageId 送信時と同じ ID
ReceiptHandle 「この受信」を表す文字列。削除と可視性タイムアウトの変更に使う。受信のたびに変わる
MD5OfBody 本文の MD5
Body 本文
Attributes システムが付ける属性。--message-system-attribute-names を指定したときだけ入る
MessageAttributes 送信側が付けた属性。--message-attribute-names を指定したときだけ入る

--max-number-of-messages は1〜10です。キューが空のとき、CLI は何も表示せずに終了し、boto3 の応答には Messages キーが入りません。boto3 では r.get("Messages", []) で受けると、空のときもそのままループに渡せます。

受信した直後の件数を見ると、メッセージは消えておらず「処理中」に移っただけだと分かります。

aws sqs get-queue-attributes --queue-url "$Q" \
  --attribute-names ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible
{
    "Attributes": {
        "ApproximateNumberOfMessages": "1",
        "ApproximateNumberOfMessagesNotVisible": "1"
    }
}

メッセージを消す(delete-message)

削除には、受信の応答に入っていた ReceiptHandle を渡します。MessageId ではない点に気をつけます。

RH=$(jq -r '.Messages[0].ReceiptHandle' msg.json)
aws sqs delete-message --queue-url "$Q" --receipt-handle "$RH"
echo "exit=$?"
aws sqs get-queue-attributes --queue-url "$Q" \
  --attribute-names ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible
exit=0
{
    "Attributes": {
        "ApproximateNumberOfMessages": "1",
        "ApproximateNumberOfMessagesNotVisible": "0"
    }
}

delete-message は成功しても何も表示しません。処理中の数が0に戻りました。boto3 では「受信 → 処理 → 削除」を1つのループにまとめます。

def handle(body):
    print("処理:", body)

msgs = sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=10, WaitTimeSeconds=1).get("Messages", [])
for m in msgs:
    handle(m["Body"])
    sqs.delete_message(QueueUrl=url, ReceiptHandle=m["ReceiptHandle"])   # 処理が終わってから消す
print("残り:", sqs.receive_message(QueueUrl=url, WaitTimeSeconds=1).get("Messages", []))
処理: {"order_id": "0001", "item": "ノート"}
残り: []

削除は処理が終わった後に行います。 先に消すと、処理の途中で止まったときにメッセージが失われます。後に消す順にしておけば、途中で止まっても次の節の仕組みで再び受信できます。

受信しただけでは消えない:可視性タイムアウトの動き

可視性タイムアウトは、受信されたメッセージが他の受信者から見えなくなる時間です。公式開発者ガイドによると既定は30秒、設定できる範囲は0秒〜12時間です。この時間内に削除されなかったメッセージは、再び受信できる状態に戻ります。

可視性タイムアウト3秒のキューで、経過秒数を付けて確かめます。

import time

T0 = time.time()
def log(msg):
    print(f"[{time.time() - T0:5.1f}s] {msg}")

def counts(u):
    a = sqs.get_queue_attributes(QueueUrl=u, AttributeNames=[
        "ApproximateNumberOfMessages", "ApproximateNumberOfMessagesNotVisible"])["Attributes"]
    return f"受信可={a['ApproximateNumberOfMessages']} 処理中={a['ApproximateNumberOfMessagesNotVisible']}"

def recv(u):
    return sqs.receive_message(QueueUrl=u, MaxNumberOfMessages=1,
                               MessageSystemAttributeNames=["ApproximateReceiveCount"]).get("Messages", [])

sqs.send_message(QueueUrl=url, MessageBody="task-1")
log("送信した直後           " + counts(url))

m = recv(url)[0]; h1 = m["ReceiptHandle"]
log(f"1回目の受信 ReceiveCount={m['Attributes']['ApproximateReceiveCount']}  " + counts(url))
log(f"すぐにもう一度受信     {len(recv(url))}件")

time.sleep(3.2)
log("3秒たった(削除せず)  " + counts(url))

m = recv(url)[0]; h2 = m["ReceiptHandle"]
log(f"2回目の受信 ReceiveCount={m['Attributes']['ApproximateReceiveCount']}  ReceiptHandle が変わった={h1 != h2}")

sqs.delete_message(QueueUrl=url, ReceiptHandle=h2)
log("削除した               " + counts(url))
time.sleep(3.2)
log(f"さらに3秒後に受信      {len(recv(url))}件")
[  0.0s] 送信した直後           受信可=1 処理中=0
[  0.0s] 1回目の受信 ReceiveCount=1  受信可=0 処理中=1
[  0.0s] すぐにもう一度受信     0件
[  3.2s] 3秒たった(削除せず)  受信可=1 処理中=0
[  3.2s] 2回目の受信 ReceiveCount=2  ReceiptHandle が変わった=True
[  3.2s] 削除した               受信可=0 処理中=0
[  6.4s] さらに3秒後に受信      0件

読み取れることは4つです。

  • 受信しても消えず、「処理中」に移るだけです。その間、別の受信には出てきません。
  • 3秒(可視性タイムアウト)を過ぎると、同じメッセージが再び受信できる状態に戻ります。
  • 2回目の受信では ApproximateReceiveCount が2になり、ReceiptHandle は新しい値に変わります。
  • 削除すると、時間がたっても戻ってきません。

このため受け取る側の処理は、同じメッセージが2回届いても結果が変わらないように作っておくのが基本です。

処理に時間がかかるとき:change-message-visibility

処理が可視性タイムアウトより長引きそうなときは、受信済みのメッセージの見えない時間を延ばせます。

aws sqs send-message --queue-url "$Q" --message-body 'long-task' --query MessageId --output text
RH=$(aws sqs receive-message --queue-url "$Q" --query 'Messages[0].ReceiptHandle' --output text)
aws sqs change-message-visibility --queue-url "$Q" --receipt-handle "$RH" --visibility-timeout 120
echo "exit=$?"
4d3b2f70-c956-4849-b3a8-014eb2398e43
exit=0

boto3 でも同じ引数です。延長している間は受信されないこと、0を指定するとすぐに受信できる状態へ戻せることを確かめます。

T0 = time.time()
sqs.send_message(QueueUrl=url, MessageBody="task-2")
m = recv(url)[0]
sqs.change_message_visibility(QueueUrl=url, ReceiptHandle=m["ReceiptHandle"], VisibilityTimeout=8)
log("8秒に延長")
time.sleep(3.2)
log(f"元の3秒を過ぎて受信    {len(recv(url))}件(延長が効いている)")

sqs.change_message_visibility(QueueUrl=url, ReceiptHandle=m["ReceiptHandle"], VisibilityTimeout=0)
m2 = recv(url)
log(f"0秒にして受信          {len(m2)}件 ReceiveCount={m2[0]['Attributes']['ApproximateReceiveCount']}")
sqs.delete_message(QueueUrl=url, ReceiptHandle=m2[0]["ReceiptHandle"])
[  0.0s] 8秒に延長
[  3.2s] 元の3秒を過ぎて受信    0件(延長が効いている)
[  3.2s] 0秒にして受信          1件 ReceiveCount=2
やりたいこと 指定
処理が長引くので延ばしたい 残したい秒数を指定(最初の受信から12時間が上限。公式開発者ガイド)
処理できなかったので、すぐ他へ回したい 0 を指定
キュー全体の既定を変えたい set-queue-attributes --attributes VisibilityTimeout=秒

何度も処理できないメッセージをDLQへ移す

形式が壊れているなど、何度受信しても処理できないメッセージが残ると、同じメッセージの受信が繰り返されます。DLQ(デッドレターキュー)を設定すると、決めた回数を超えて受信されたメッセージを別のキューへ移せます。

設定は、元のキューの RedrivePolicy 属性に、移動先キューの ARN と maxReceiveCount(受信できる回数)を JSON で入れます。

dlq = sqs.create_queue(QueueName="demo-dlq")["QueueUrl"]
dlq_arn = sqs.get_queue_attributes(QueueUrl=dlq, AttributeNames=["QueueArn"])["Attributes"]["QueueArn"]
main = sqs.create_queue(QueueName="demo-main", Attributes={
    "VisibilityTimeout": "2",
    "RedrivePolicy": json.dumps({"deadLetterTargetArn": dlq_arn, "maxReceiveCount": "3"}),
})["QueueUrl"]

sqs.send_message(QueueUrl=main, MessageBody="broken-payload")
T0 = time.time()
for i in range(1, 6):
    ms = recv(main)
    got = f"ReceiveCount={ms[0]['Attributes']['ApproximateReceiveCount']}(削除しない)" if ms else "0件"
    log(f"受信{i}回目: {got} / 元のキュー {counts(main)} / DLQ {counts(dlq)}")
    time.sleep(2.2)

ms = sqs.receive_message(QueueUrl=dlq, WaitTimeSeconds=1).get("Messages", [])
log(f"DLQ から受信: Body={ms[0]['Body']}")
[  0.0s] 受信1回目: ReceiveCount=1(削除しない) / 元のキュー 受信可=0 処理中=1 / DLQ 受信可=0 処理中=0
[  2.2s] 受信2回目: ReceiveCount=2(削除しない) / 元のキュー 受信可=0 処理中=1 / DLQ 受信可=0 処理中=0
[  4.4s] 受信3回目: ReceiveCount=3(削除しない) / 元のキュー 受信可=0 処理中=1 / DLQ 受信可=0 処理中=0
[  6.7s] 受信4回目: 0件 / 元のキュー 受信可=0 処理中=0 / DLQ 受信可=1 処理中=0
[  8.9s] 受信5回目: 0件 / 元のキュー 受信可=0 処理中=0 / DLQ 受信可=1 処理中=0
[ 11.1s] DLQ から受信: Body=broken-payload

3回目までは元のキューで受信でき、4回目の受信のタイミングで DLQ へ移り、元のキューからは受信されなくなりました。DLQ に入ったメッセージは、通常のキューと同じく receive-message で取り出して中身を調べられます。

CLI で設定する場合は、JSON の中に JSON 文字列を入れる形になります。

DLQ=$(aws sqs create-queue --queue-name orders-dlq --query QueueUrl --output text)
ARN=$(aws sqs get-queue-attributes --queue-url "$DLQ" --attribute-names QueueArn --query Attributes.QueueArn --output text)
aws sqs set-queue-attributes --queue-url "$Q" \
  --attributes "{\"RedrivePolicy\":\"{\\\"deadLetterTargetArn\\\":\\\"$ARN\\\",\\\"maxReceiveCount\\\":\\\"5\\\"}\"}"
aws sqs get-queue-attributes --queue-url "$Q" --attribute-names RedrivePolicy
{
    "Attributes": {
        "RedrivePolicy": "{\"deadLetterTargetArn\": \"arn:aws:sqs:ap-northeast-1:123456789012:orders-dlq\", \"maxReceiveCount\": 5}"
    }
}

公式開発者ガイドで確認できた、DLQ の決まりと勧められている設定です。

項目 内容
キューの種類 標準キューの DLQ は標準キュー、FIFO キューの DLQ は FIFO キューにする
maxReceiveCount 一時的なエラーで再試行できる余地が残る回数にする
保持期間 DLQ の保持期間は、元のキューより長くすることが勧められている
戻す DLQ のメッセージを元のキューへ戻す機能(リドライブ)がある

待ち方を選ぶ:ロングポーリング

receive-message の WaitTimeSeconds は、キューが空のときに待つ秒数です。0ならすぐに返り(ショートポーリング)、1〜20なら届くまで待ちます(ロングポーリング)。

import threading

empty = sqs.create_queue(QueueName="demo-empty")["QueueUrl"]
for w in (0, 3):
    t = time.time()
    r = sqs.receive_message(QueueUrl=empty, WaitTimeSeconds=w)
    print(f"WaitTimeSeconds={w}: {time.time() - t:.1f}秒で応答 Messages あり={'Messages' in r}")

threading.Timer(1.0, lambda: boto3.client("sqs").send_message(QueueUrl=empty, MessageBody="late")).start()
t = time.time()
r = sqs.receive_message(QueueUrl=empty, WaitTimeSeconds=10)
print(f"WaitTimeSeconds=10・1秒後に送信: {time.time() - t:.1f}秒で応答 {len(r.get('Messages', []))}件")
WaitTimeSeconds=0: 0.0秒で応答 Messages あり=False
WaitTimeSeconds=3: 3.0秒で応答 Messages あり=False
WaitTimeSeconds=10・1秒後に送信: 1.0秒で応答 1件

10秒待つ指定でも、メッセージが届けばその時点で返ります。公式開発者ガイドによると、ショートポーリングは一部のサーバーだけを調べるため、メッセージがあっても空の応答になることがあり、ロングポーリングは全サーバーを調べて空の応答を減らします。受信ループでは WaitTimeSeconds=20 を基本にすると、呼び出し回数も抑えられます。

待ち時間はキューの属性 ReceiveMessageWaitTimeSeconds で既定値にもでき、受信時に WaitTimeSeconds を指定した場合はそちらが優先されます。

まとめて送る・消す・全部消す

aws sqs send-message-batch --queue-url "$Q" \
  --entries '[{"Id":"a","MessageBody":"m1"},{"Id":"b","MessageBody":"m2"}]' \
  --query '{Successful:Successful[].Id,Failed:Failed}'
{
    "Successful": [
        "a",
        "b"
    ],
    "Failed": []
}

Id は応答と突き合わせるための、リクエスト内の目印です(応答には各メッセージの MessageId と MD5 も入ります)。1回で送れるのは10件までで、11件渡すと次のエラーになります。受信したメッセージは delete_message_batch でまとめて消せます。

r = sqs.send_message_batch(QueueUrl=empty, Entries=[{"Id": str(i), "MessageBody": f"m{i}"} for i in range(10)])
print("成功", len(r["Successful"]), "エラー", len(r.get("Failed", [])))

from botocore.exceptions import ClientError
try:
    sqs.send_message_batch(QueueUrl=empty, Entries=[{"Id": str(i), "MessageBody": f"m{i}"} for i in range(11)])
except ClientError as e:
    print(e.response["Error"]["Code"], "|", e.response["Error"]["Message"])

total = 0
while True:
    ms = sqs.receive_message(QueueUrl=empty, MaxNumberOfMessages=10, WaitTimeSeconds=1).get("Messages", [])
    if not ms:
        break
    r = sqs.delete_message_batch(QueueUrl=empty, Entries=[
        {"Id": m["MessageId"], "ReceiptHandle": m["ReceiptHandle"]} for m in ms])
    total += len(r["Successful"])
print("受信して削除した件数:", total)
成功 10 エラー 0
AWS.SimpleQueueService.TooManyEntriesInBatchRequest | Maximum number of entries per request are 10. You have sent 11.
受信して削除した件数: 10

バッチの応答は、全体が成功でも Failed に個別のエラーが入ることがあるので、Failed が空かどうかを確認します。

キューの中身を全部消すのが purge-queue です。処理中のメッセージも含めて消えます。

aws sqs purge-queue --queue-url "$Q"
echo "exit=$?"
aws sqs get-queue-attributes --queue-url "$Q" \
  --attribute-names ApproximateNumberOfMessages ApproximateNumberOfMessagesNotVisible
exit=0
{
    "Attributes": {
        "ApproximateNumberOfMessages": "0",
        "ApproximateNumberOfMessagesNotVisible": "0"
    }
}

boto3 では sqs.purge_queue(QueueUrl=url) です。元に戻せない操作なので、テスト用のキューを空にする用途に向いています。

標準キューとFIFOキューの違い

観点 標準キュー FIFO キュー
順序 できるだけ送信順(保証はされない) メッセージグループの中で送信順を保つ
重複 少なくとも1回届く(2回以上届くことがある) 5分間の重複排除で、同じメッセージを1回だけ届ける
キュー名 英数字・ハイフン・アンダースコア 末尾を .fifo にする
作成時の属性 なし FifoQueue=true
MessageGroupId - 送信時に必須
MessageDeduplicationId - 必須(ContentBasedDeduplication が有効なら省略できる)
スループット ほぼ無制限 API ごとに毎秒300回、バッチで毎秒3,000メッセージ(高スループットモードあり)
向いている場面 順序を問わない大量の処理 同じ対象への操作を順番どおりに処理したいとき

順序・重複・スループットの行は公式開発者ガイド「Amazon SQS queue types」の記述、それ以外は以下で動かした結果です。

FIFOキューを作って送る

echo "--- 名前が .fifo で終わっていない"
aws sqs create-queue --queue-name tasks --attributes FifoQueue=true
echo "--- 正しい作り方"
aws sqs create-queue --queue-name tasks.fifo --attributes FifoQueue=true
F=$(aws sqs get-queue-url --queue-name tasks.fifo --query QueueUrl --output text)
--- 名前が .fifo で終わっていない

An error occurred (InvalidParameterValue) when calling the CreateQueue operation: Queue name must end in .fifo for FIFO queues
--- 正しい作り方
{
    "QueueUrl": "https://sqs.ap-northeast-1.amazonaws.com/123456789012/tasks.fifo"
}

MessageGroupIdを省いたときのエラー

echo "--- MessageGroupId なし"
aws sqs send-message --queue-url "$F" --message-body 'step1'
echo "--- MessageGroupId あり・MessageDeduplicationId なし"
aws sqs send-message --queue-url "$F" --message-body 'step1' --message-group-id C001
echo "--- 両方あり"
aws sqs send-message --queue-url "$F" --message-body 'step1' \
  --message-group-id C001 --message-deduplication-id d-001
--- MessageGroupId なし

An error occurred (MissingParameter) when calling the SendMessage operation: The request must contain the parameter MessageGroupId.
--- MessageGroupId あり・MessageDeduplicationId なし

An error occurred (InvalidParameterValue) when calling the SendMessage operation: The queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly
--- 両方あり
{
    "MD5OfMessageBody": "2e957eb8c030fc2d10de5607715f4c55",
    "MessageId": "953e8154-df47-4c9b-9d08-edd0b53c26bd",
    "SequenceNumber": "84826278019568613503"
}
エラー 直し方
MissingParameter: The request must contain the parameter MessageGroupId. --message-group-id(boto3 は MessageGroupId=)を付ける
InvalidParameterValue: The queue should either have ContentBasedDeduplication enabled or MessageDeduplicationId provided explicitly --message-deduplication-id を付けるか、キューの ContentBasedDeduplication を true にする
InvalidParameterValue: Queue name must end in .fifo for FIFO queues キュー名の末尾を .fifo にする

FIFO の送信が成功すると、応答に SequenceNumber が加わります。

重複排除の動き

同じ MessageDeduplicationId でもう一度送ると、送信は受け付けられますが、キューには追加されません。公式開発者ガイドによると、この重複排除は5分間はたらきます。

aws sqs send-message --queue-url "$F" --message-body 'step1' \
  --message-group-id C001 --message-deduplication-id d-001 --query MD5OfMessageBody --output text
aws sqs get-queue-attributes --queue-url "$F" --attribute-names ApproximateNumberOfMessages \
  --query Attributes.ApproximateNumberOfMessages --output text
2e957eb8c030fc2d10de5607715f4c55
1

2回送っても、キューにあるのは1件です。ContentBasedDeduplication を有効にすると、本文から重複排除用の ID が自動で作られ、MessageDeduplicationId を省けます。

aws sqs set-queue-attributes --queue-url "$F" --attributes ContentBasedDeduplication=true
aws sqs send-message --queue-url "$F" --message-body 'step2' --message-group-id C001 --query MD5OfMessageBody --output text
aws sqs send-message --queue-url "$F" --message-body 'step2' --message-group-id C001 --query MD5OfMessageBody --output text
aws sqs receive-message --queue-url "$F" --max-number-of-messages 10 \
  --message-system-attribute-names All \
  --query 'Messages[].[Body,Attributes.MessageGroupId,Attributes.MessageDeduplicationId]' --output json
9b782e40768fc9007786b032ba7911aa
9b782e40768fc9007786b032ba7911aa
[
    [
        "step1",
        "C001",
        "d-001"
    ],
    [
        "step2",
        "C001",
        "99cb29fd2d2e3f41f1923c47972861b3beb6852cfb8e608950d83441b7b4b904"
    ]
]

同じ本文の step2 を2回送りましたが、届いたのは1件です。

メッセージグループと順序

FIFO キューの順序は、MessageGroupId が同じメッセージの間で保たれます。2つのグループを交互に送って、受信の順を確かめます。

fifo = sqs.create_queue(QueueName="demo.fifo", Attributes={
    "FifoQueue": "true", "ContentBasedDeduplication": "true"})["QueueUrl"]
for g, b in [("C001", "A-1"), ("C002", "B-1"), ("C001", "A-2"), ("C001", "A-3"), ("C002", "B-2")]:
    sqs.send_message(QueueUrl=fifo, MessageBody=b, MessageGroupId=g)

def recv_fifo(n):
    ms = sqs.receive_message(QueueUrl=fifo, MaxNumberOfMessages=n,
                             MessageSystemAttributeNames=["MessageGroupId"]).get("Messages", [])
    return ms, [(m["Attributes"]["MessageGroupId"], m["Body"]) for m in ms]

ms1, v = recv_fifo(1);  print("1件だけ受信        :", v)
ms2, v = recv_fifo(10); print("A-1 を消す前に受信 :", v)
for m in ms1 + ms2:
    sqs.delete_message(QueueUrl=fifo, ReceiptHandle=m["ReceiptHandle"])
ms3, v = recv_fifo(10); print("削除した後に受信   :", v)
1件だけ受信        : [('C001', 'A-1')]
A-1 を消す前に受信 : [('C002', 'B-1'), ('C002', 'B-2')]
削除した後に受信   : [('C001', 'A-2'), ('C001', 'A-3')]

グループ C001 の先頭(A-1)が処理中の間、同じグループの A-2 と A-3 は受信されず、別グループの C002 だけが届きました。A-1 を削除すると、続きの A-2、A-3 が順番どおりに届きます。順番を守りたい単位(顧客 ID や注文 ID など)を MessageGroupId にするのが設計の要点です。全メッセージを同じグループにすると全体が1列になり、並行して処理できる数が1つに限られます。

属性の早見表

キューの属性

set-queue-attributes や create-queue の --attributes で指定します。範囲と既定値は公式 API リファレンス(SetQueueAttributes)の記載です。

属性 意味 範囲 既定
VisibilityTimeout 受信後に見えなくなる秒数 0〜43,200(12時間) 30
MessageRetentionPeriod メッセージを保持する秒数 60〜1,209,600(14日) 345,600(4日)
DelaySeconds 送信後、受信できるようになるまでの秒数 0〜900(15分) 0
ReceiveMessageWaitTimeSeconds 受信時に待つ秒数の既定 0〜20 0
MaximumMessageSize メッセージの最大サイズ(バイト) 1,024〜1,048,576 1,048,576
RedrivePolicy DLQ の ARN と maxReceiveCount の JSON - なし
FifoQueue FIFO キューにする(作成時のみ) true -
ContentBasedDeduplication 本文から重複排除 ID を作る(FIFO のみ) true / false false

属性の変更が全体に行き渡るまでには最大60秒、MessageRetentionPeriod は最大15分かかることがあると、同リファレンスに書かれています。

受信メッセージのシステム属性

属性 意味
ApproximateReceiveCount これまでに受信された回数。DLQ へ移す判定に使われる
SentTimestamp 送信された時刻(ミリ秒)
ApproximateFirstReceiveTimestamp 最初に受信された時刻(ミリ秒)
SenderId 送信した IAM の ID
MessageGroupId FIFO のグループ ID
MessageDeduplicationId FIFO の重複排除 ID
SequenceNumber FIFO でメッセージに付く連番

エラーと例外処理

boto3 では、エラーは botocore.exceptions.ClientError として届き、e.response["Error"]["Code"] で種類を見分けます。

def show(label, f):
    try:
        f(); print(label, "-> OK")
    except ClientError as e:
        print(f"{label}\n  -> {type(e).__name__} / Code={e.response['Error']['Code']}\n     {e.response['Error']['Message']}")
    except Exception as e:
        print(f"{label}\n  -> {type(e).__name__}: {e}")

show("存在しないキュー名で get_queue_url", lambda: sqs.get_queue_url(QueueName="no-such-queue"))
show("でたらめな ReceiptHandle で削除", lambda: sqs.delete_message(QueueUrl=url, ReceiptHandle="invalid"))
show("本文に dict を渡す", lambda: sqs.send_message(QueueUrl=url, MessageBody={"a": 1}))
show("同じ名前・違う属性で create_queue",
     lambda: sqs.create_queue(QueueName="demo-queue", Attributes={"VisibilityTimeout": "10"}))
show("同じ名前・同じ属性で create_queue",
     lambda: sqs.create_queue(QueueName="demo-queue", Attributes={"VisibilityTimeout": "3"}))
show("FIFO で MessageGroupId なし", lambda: sqs.send_message(QueueUrl=fifo, MessageBody="x"))
存在しないキュー名で get_queue_url
  -> QueueDoesNotExist / Code=AWS.SimpleQueueService.NonExistentQueue
     The specified queue does not exist.
でたらめな ReceiptHandle で削除
  -> ReceiptHandleIsInvalid / Code=ReceiptHandleIsInvalid
     The input receipt handle is invalid.
本文に dict を渡す
  -> ParamValidationError: Parameter validation failed:
Invalid type for parameter MessageBody, value: {'a': 1}, type: <class 'dict'>, valid types: <class 'str'>
同じ名前・違う属性で create_queue
  -> QueueNameExists / Code=QueueAlreadyExists
     The specified queue already exists.
同じ名前・同じ属性で create_queue -> OK
FIFO で MessageGroupId なし
  -> ClientError / Code=MissingParameter
     The request must contain the parameter MessageGroupId.
Code 状況 直し方
AWS.SimpleQueueService.NonExistentQueue キューが無い、またはリージョンが違う aws sqs list-queues で名前とリージョンを確認
ReceiptHandleIsInvalid ReceiptHandle の値が正しくない 受信の応答の ReceiptHandle をそのまま渡す(MessageId ではない)
QueueAlreadyExists 同じ名前で属性の違うキューを作ろうとした 属性をそろえるか、set-queue-attributes で変更する
MissingParameter FIFO で MessageGroupId が無い MessageGroupId を付ける
ParamValidationError(通信前) 本文が文字列でない json.dumps で文字列にする

同じ名前・同じ属性の create_queue はエラーにならず、既存の URL が返ります。 起動時に「無ければ作る」処理として安心して呼べます。

キューが無いときの例外クラスは、上の出力のとおり QueueDoesNotExist です。コードの文字列は長い形なので、except sqs.exceptions.QueueDoesNotExist: と例外クラスで受けると読みやすくなります。

受信ループ全体の形は次のとおりです。処理で例外が出たメッセージは削除せず、可視性タイムアウトと DLQ に任せます。

def worker(queue_url, handler, max_loops):
    for _ in range(max_loops):
        msgs = sqs.receive_message(QueueUrl=queue_url, MaxNumberOfMessages=10,
                                   WaitTimeSeconds=1).get("Messages", [])
        for m in msgs:
            try:
                handler(m["Body"])
            except Exception as e:
                print(f"  処理できず(消さずに残す): {m['Body']} / {e}")
                continue
            sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=m["ReceiptHandle"])
            print(f"  完了して削除: {m['Body']}")

def handler(body):
    if body == "bad":
        raise ValueError("形式が違います")

for b in ["ok-1", "bad", "ok-2"]:
    sqs.send_message(QueueUrl=url, MessageBody=b)
worker(url, handler, max_loops=1)
print(counts(url))
  完了して削除: ok-1
  処理できず(消さずに残す): bad / 形式が違います
  完了して削除: ok-2
受信可=0 処理中=1

掲載コードでは確認のため待ち時間を1秒、ループを1回にしています。常時動かす処理では WaitTimeSeconds=20 にして、ループを続ける形にします。

よくある質問

Q. 受信したのにメッセージが消えないのはなぜですか? A. SQS は受信しただけでは消さない仕組みだからです。処理が終わったら、受信の応答に入っている ReceiptHandle を渡して delete-message を呼びます。消さずにいると、可視性タイムアウトの後に再び受信されます。

Q. 標準キューと FIFO キューはどちらを選べばよいですか? A. 順番が入れ替わっても、同じメッセージが2回届いても困らない処理なら標準キューが向いています。同じ対象への操作を送信した順に処理したい場合は FIFO キューを選び、順番を守りたい単位を MessageGroupId にします。

Q. ReceiptHandle と MessageId はどう違いますか? A. MessageId はメッセージそのものの ID で、何度受信しても同じです。ReceiptHandle は受信のたびに発行される値で、削除や可視性タイムアウトの変更にはこちらを使います。

まとめ

  • SQS の基本は「送る・受け取る・消す」の3操作。 受信しただけでは消えず、ReceiptHandle を渡して delete-message を呼ぶ
  • 削除しないまま可視性タイムアウト(既定30秒)が過ぎると、同じメッセージが再び受信できる状態に戻る
  • 処理が長引くときは change-message-visibility で延ばし、0を指定するとすぐ他へ回せる
  • 何度も受信されるメッセージは、RedrivePolicy の maxReceiveCount を超えた時点で DLQ へ移る
  • 受信ループは WaitTimeSeconds=20 のロングポーリングが基本。 空のときは Messages キーが無いので get("Messages", []) で受ける
  • FIFO キューは名前の末尾が .fifo、送信に MessageGroupId が必須。 省くと MissingParameter になる
  • FIFO の順序はメッセージグループ単位。 順番を守りたい単位を MessageGroupId にする

参考・出典

  • Amazon SQS 開発者ガイド「Amazon SQS visibility timeout」(既定30秒、上限12時間、受信では削除されないこと)
  • Amazon SQS 開発者ガイド「Amazon SQS queue types」(標準と FIFO の順序・配信・スループット、.fifo の命名)
  • Amazon SQS 開発者ガイド「Using dead-letter queues in Amazon SQS」(maxReceiveCount、キューの種類、保持期間、リドライブ)
  • Amazon SQS 開発者ガイド「Amazon SQS short and long polling」(待ち時間の上限20秒、指定の優先順位)
  • Amazon SQS 開発者ガイド「Using the Amazon SQS message deduplication ID」(重複排除の5分間)
  • Amazon SQS API リファレンス「SetQueueAttributes」(各属性の範囲と既定値、変更が行き渡るまでの時間)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Amazon SQSの使い方|送受信・削除と可視性タイムアウト、FIFO」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?