SQS message group IDs don’t guarantee order between groups, but they do guarantee order within a single group.

Here’s how to see it in action. Imagine you have a producer sending messages to an SQS queue, and a consumer processing them.

import boto3

sqs = boto3.client('sqs', region_name='us-east-1')
queue_url = 'YOUR_QUEUE_URL' # Replace with your actual queue URL

# Producer
def send_messages():
    messages = [
        {'Id': 'msg1', 'MessageBody': '{"order": 1, "group": "A"}', 'MessageGroupId': 'A'},
        {'Id': 'msg2', 'MessageBody': '{"order": 2, "group": "B"}', 'MessageGroupId': 'B'},
        {'Id': 'msg3', 'MessageBody': '{"order": 3, "group": "A"}', 'MessageGroupId': 'A'},
        {'Id': 'msg4', 'MessageBody': '{"order": 4, "group": "A"}', 'MessageGroupId': 'A'},
        {'Id': 'msg5', 'MessageBody': '{"order": 5, "group": "B"}', 'MessageGroupId': 'B'},
    ]
    response = sqs.send_message_batch(
        QueueUrl=queue_url,
        Entries=messages
    )
    print("Sent messages:", response)

# Consumer
def receive_messages():
    response = sqs.receive_message(
        QueueUrl=queue_url,
        MaxNumberOfMessages=10,
        WaitTimeSeconds=20, # Long polling
        MessageAttributeNames=['All'],
        AttributeNames=['All']
    )
    messages = response.get('Messages', [])
    if not messages:
        print("No messages received.")
        return

    for msg in messages:
        print(f"Received: {msg['MessageBody']} (Group ID: {msg['Attributes']['MessageGroupId']})")
        # In a real application, you'd process the message and then delete it
        sqs.delete_message(
            QueueUrl=queue_url,
            ReceiptHandle=msg['ReceiptHandle']
        )
    print(f"Processed {len(messages)} messages.")

# To run:
# send_messages()
# receive_messages()

When you run send_messages(), you’re assigning a MessageGroupId to each message. Notice how messages 1, 3, and 4 all have MessageGroupId: 'A', and messages 2 and 5 have MessageGroupId: 'B'.

When receive_messages() runs, SQS guarantees that all messages with MessageGroupId: 'A' will be delivered in the order they were sent relative to each other. Similarly, all messages with MessageGroupId: 'B' will be delivered in their sent order. However, there’s no guarantee that message 1 (group A) will arrive before message 2 (group B), or vice versa. The consumer might receive {"order": 2, "group": "B"} before {"order": 1, "group": "A"}.

The core problem this solves is maintaining order for related events without requiring a single consumer to handle all events, which would be a bottleneck. By sharding your workload across groups, you enable parallel processing while still preserving the integrity of ordered sequences. Each message group is processed by at most one consumer at any given time. If you have multiple consumers polling the same queue, SQS ensures that only one consumer can receive and process messages from a specific message group. This is the mechanism that enforces order within the group.

The magic lies in SQS’s internal routing. When a message is sent with a MessageGroupId, SQS tags it. When a consumer polls, SQS looks at the available messages and, for each message group that has pending messages, it assigns one of those messages to the polling consumer. Crucially, it then "locks" that message group to that specific consumer until the message is deleted or visibility timeout expires. This ensures that no other consumer can pick up another message from the same group until the first one is resolved.

The MessageDeduplicationId is also key here. If you’re sending messages with the same MessageGroupId and the same MessageDeduplicationId within a 5-minute interval, SQS will deduplicate them. This is separate from ordering but often used in conjunction. For strict ordering, you’d typically use a sequence number as the MessageDeduplicationId.

The most surprising thing is how SQS implements this. It’s not a strict FIFO queue by default. Standard queues offer at-least-once delivery and best-effort ordering. To get strict ordering and exactly-once processing, you need to use FIFO queues. However, even with standard queues, the MessageGroupId provides a powerful mechanism for within-group ordering, which is sufficient for many use cases where you need to process related events in sequence but can handle inter-group events out of order. The distinction between standard and FIFO queues is critical here, as FIFO queues offer stronger guarantees but come with throughput limitations.

The next concept you’ll likely encounter is understanding how to manage visibility timeouts effectively to avoid message redelivery when processing is complex.

Want structured learning?

Take the full Sqs course →