SQS: sending and receiving messages

boto3 · Practical · 12 min

Queues decouple services, and getting receive/delete semantics wrong causes duplicate processing or lost work.

An SQS queue stores messages until a consumer deletes them. You address it by QueueUrl (get_queue_url(QueueName=...) looks it up).

  • send_message(QueueUrl=..., MessageBody="...") enqueues a message (up to 256 KB of text; send JSON with json.dumps). send_message_batch sends up to 10 at once.
  • receive_message(QueueUrl=..., MaxNumberOfMessages=10, WaitTimeSeconds=20) returns up to 10 messages. WaitTimeSeconds enables long polling: the call waits up to 20 seconds for messages instead of returning empty immediately, which cuts cost and latency.
  • delete_message(QueueUrl=..., ReceiptHandle=msg["ReceiptHandle"]) acknowledges a message after successful processing.

Receiving does not remove a message. It becomes invisible for the queue's visibility timeout (default 30 seconds); if you don't delete it in time, it reappears for another consumer. So: process, then delete; set the visibility timeout longer than processing takes, or extend it with change_message_visibility.

Standard queues deliver at least once and don't guarantee order: handlers must be idempotent. FIFO queues (names ending in .fifo) preserve order within a MessageGroupId and deduplicate within 5 minutes using a MessageDeduplicationId or content-based deduplication.

Configure a dead-letter queue (a redrive policy with maxReceiveCount) so poison messages stop cycling forever.

Example

import json
import boto3

sqs = boto3.client("sqs", region_name="us-east-1")
url = sqs.create_queue(QueueName="orders",
                       Attributes={"VisibilityTimeout": "60"})["QueueUrl"]

sqs.send_message_batch(QueueUrl=url, Entries=[
    {"Id": str(i), "MessageBody": json.dumps({"order_id": i})} for i in range(3)])

def handle(order):
    print("processing", order["order_id"])

while True:
    resp = sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=10, WaitTimeSeconds=1)
    messages = resp.get("Messages", [])
    if not messages:
        break                                  # real workers keep polling (WaitTimeSeconds=20)
    for msg in messages:
        handle(json.loads(msg["Body"]))
        sqs.delete_message(QueueUrl=url, ReceiptHandle=msg["ReceiptHandle"])
# processing 0 / processing 1 / processing 2   (order not guaranteed)

Remember

  • receive_message doesn't remove messages; delete after processing
  • Long polling with WaitTimeSeconds=20 reduces empty responses and cost
  • Visibility timeout must exceed processing time
  • Standard queues are at-least-once and unordered; FIFO queues order per group

Watch out for

  • Deleting the message before processing it, losing work when the handler crashes.

Check yourself

  1. A consumer receives a message, crashes, and never deletes it. What happens?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: boto3.amazonaws.com/v1/documentation/api/latest/guide/sqs.html