SQS Message Attributes vs Message Body: Why Attributes Come Back Empty, the 10-Attribute Limit, and the SNS Raw Delivery Rule That Drops Messages
An Amazon SQS message has a body, the data your consumer works on, and up to 10 message attributes, small typed labels sent alongside it, such as OrderType=repair or Priority=2. Put the data in the body; put things you route, filter or trace by in attributes, so a consumer can decide what to do without parsing the body. The first surprise is the one behind most "my attributes are missing" questions: SQS doesn't return message attributes unless you ask for them. A receive-message without --message-attribute-names All brings back the body and no attributes at all, even though they're stored. The second is worse, because it loses messages: when SNS delivers to SQS with raw message delivery turned on, a message with more than 10 attributes is discarded. And without raw delivery, the attributes aren't SQS attributes at all; they're buried inside the body's JSON. This page shows how attributes work, the rules and limits, and working code for the CLI, Python, Node.js and Lambda, including a consumer that reads attributes from both kinds of SNS delivery.
Ethan, a developer, moved the order flow for his friend Jake's phone repair shop onto SQS: the website sends each repair order to a queue, and a worker picks it up. He tagged every message with an OrderType attribute so the worker could hand screen repairs to one technician and battery jobs to another. The worker never saw a single tag. Every message arrived with its body and nothing else.
New to SQS? It's AWS's message queue: one program puts messages in, another takes them out and processes them, and the queue holds them safely in between. Our SQS guide covers the basics.
Message body vs message attributes: what goes where
Both travel together and both count toward the message's size, but they do different jobs:
| Message body | Message attributes | |
|---|---|---|
| What it's for | The data: the order, the event, the job | Labels about the data: type, priority, source, a trace ID |
| Format | Text: JSON, XML or anything else | Up to 10 name, type and value entries, typed as String, Number or Binary |
| Returned by ReceiveMessage | Always | Only if you ask with MessageAttributeNames |
| Read without parsing the body | No | Yes |
| SNS filter policies | Only with FilterPolicyScope=MessageBody | Yes, the default |
A simple rule works for almost every design: if your consumer needs it to do the work, it goes in the body. If something needs it to decide where the work goes, or to trace it, it's an attribute. Ethan's order details, the device and the fault, belong in the body; OrderType and Priority, which decide which technician gets the job, are attributes.
Jake: "So the attribute is the sticky note on the box, and the body is what's inside."
Ethan: "Exactly. You read the note to decide which bench it goes to, without opening the box."
Send a message with attributes
With the AWS CLI, attributes go in --message-attributes as JSON. Each one has a DataType and a value:
aws sqs send-message --queue-url "$Q" --message-body '{"order":"A-1042","device":"Pixel","issue":"screen"}' --message-attributes '{"OrderType":{"DataType":"String","StringValue":"repair"},"Priority":{"DataType":"Number","StringValue":"2"},"Store":{"DataType":"String.code","StringValue":"BLR-01"}}'
Two details in that command. A Number attribute still uses StringValue: you send numbers as text, and the type tells consumers to read it as a number. And String.code shows a custom type: you can add your own label after the base type, such as Number.int or String.code, to tell consumers how to read the value.
The same message from Node.js with the AWS SDK v3:
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
const sqs = new SQSClient({});
const queueUrl = process.argv[2];
const { MessageId } = await sqs.send(
new SendMessageCommand({
QueueUrl: queueUrl,
MessageBody: JSON.stringify({ order: "A-1044", device: "Galaxy", issue: "battery" }),
MessageAttributes: {
OrderType: { DataType: "String", StringValue: "repair" },
Priority: { DataType: "Number", StringValue: "1" },
},
})
);
console.log("sent", MessageId);
Run it as node send.mjs QUEUE_URL. In Python, boto3's send_message takes the same MessageAttributes dictionary.
Why your attributes come back empty
This is where Ethan's tags went. ReceiveMessage only returns message attributes you ask for by name. Receive without asking, and you get the body alone:
aws sqs receive-message --queue-url "$Q" --query "Messages[0].{Body:Body,Attrs:MessageAttributes}"
{
"Body": "{\"order\":\"A-1042\",\"device\":\"Pixel\",\"issue\":\"screen\"}",
"Attrs": null
}
Ask for them, and they're all there:
aws sqs receive-message --queue-url "$Q" --message-attribute-names All --query "Messages[0].MessageAttributes"
{
"OrderType": {
"StringValue": "repair",
"DataType": "String"
},
"Priority": {
"StringValue": "2",
"DataType": "Number"
},
"Store": {
"StringValue": "BLR-01",
"DataType": "String.code"
}
}
All and .* both mean every attribute; you can also list names, or use a prefix such as Order.*. Don't confuse this with --attribute-names (or MessageSystemAttributeNames), which asks for SQS's own system attributes, such as the receive count and timestamps. Setting that one and wondering why your custom attributes are still missing is the second most common version of this problem.
When attributes seem to be missing, check in this order:
- Your receive call passes
MessageAttributeNames, not onlyAttributeNames. - The producer really sent them: SendMessage returns an
MD5OfMessageAttributesvalue only when the message had attributes. - If the queue is fed by SNS, raw message delivery is on, or your code reads the attributes from the body.
- Your code reads the right capitalization for where it runs:
StringValuefrom the SQS API,stringValuein a Lambda event.
Jake: "So the notes were on the boxes the whole time. The worker just never asked to see them."
Ethan: "One parameter. I'd spent an hour checking the sender."
Read attributes in Python, from SQS or SNS
This consumer asks for every attribute, and it also handles messages that arrived from SNS without raw delivery, where the attributes are inside the body. More on that difference in the next section:
import json
import sys
import boto3
sqs = boto3.client("sqs")
queue_url = sys.argv[1]
resp = sqs.receive_message(
QueueUrl=queue_url,
MaxNumberOfMessages=10,
WaitTimeSeconds=5,
MessageAttributeNames=["All"], # without this, MessageAttributes never comes back
)
for msg in resp.get("Messages", []):
attrs = {k: v["StringValue"] for k, v in msg.get("MessageAttributes", {}).items() if "StringValue" in v}
body = json.loads(msg["Body"])
if body.get("Type") == "Notification": # SNS without raw delivery: attributes are inside the body
attrs.update({k: v["Value"] for k, v in body.get("MessageAttributes", {}).items()})
body = json.loads(body["Message"])
print(f"order {body['order']}: {attrs}")
sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=msg["ReceiptHandle"])
Run it as python3 consume.py QUEUE_URL. From a queue fed directly or with SNS raw delivery, and from one fed by SNS without raw delivery, it prints the same thing:
order A-1042: {'OrderType': 'repair'}
order A-1043: {'OrderType': 'repair', 'Priority': '1'}
Binary attributes carry BinaryValue instead of StringValue, which is why the first dictionary line skips them; add them separately if you use them.
SNS to SQS: raw delivery and the 10-attribute trap
When an SNS topic delivers to an SQS queue, what arrives depends on one subscription setting, raw message delivery:
- Off (the default): the SQS message's body is SNS's JSON envelope: your text under
Message, your attributes underMessageAttributes, each as aTypeand aValue, plus SNS fields such asTopicArnandSignature. The SQS message itself has no message attributes. - On: the body is exactly what you published, and your SNS attributes arrive as real SQS message attributes.
Here's the same published message, with one attribute, arriving at two queues. With raw delivery on:
{
"Body": "{\"order\":\"A-1042\"}",
"Attrs": {
"OrderType": {
"StringValue": "repair",
"DataType": "String"
}
}
}
With raw delivery off, the SQS attributes are empty, and the body holds the envelope:
Attrs: None
Body keys: ['Message', 'MessageAttributes', 'MessageId', 'Signature', 'SignatureVersion', 'SigningCertURL', 'Timestamp', 'TopicArn', 'Type', 'UnsubscribeURL']
Body.MessageAttributes: {'OrderType': {'Type': 'String', 'Value': 'repair'}}
To set up an SNS topic that feeds a queue with attributes intact:
- Subscribe the queue to the topic, and give the queue a policy that lets the topic send to it.
- Turn on raw message delivery for that subscription, with the command below.
- Keep every published message to 10 attributes or fewer.
- Add a filter policy if the queue should only get some messages.
Turn raw delivery on for a subscription with:
aws sns set-subscription-attributes --subscription-arn "$SUBSCRIPTION_ARN" --attribute-name RawMessageDelivery --attribute-value true
And here's the trap. SQS messages can't have more than 10 attributes, so with raw delivery on, SNS can only pass along 10. A message published with more than 10 attributes to a topic with a raw-delivery SQS subscription isn't trimmed or delayed: SNS discards it for that subscription and treats it as a client-side error. If some messages never arrive and others do, count their attributes. Keep SNS messages to 10 attributes or fewer, or turn raw delivery off for that subscription and read the attributes from the body, as the consumer above does.
Filtering on attributes or on the body
SNS subscription filter policies look at message attributes by default, so a subscription can receive only repair orders without anyone parsing the body:
aws sns set-subscription-attributes --subscription-arn "$SUBSCRIPTION_ARN" --attribute-name FilterPolicy --attribute-value '{"OrderType":["repair"]}'
To filter on fields inside a JSON body instead, set the subscription's FilterPolicyScope to MessageBody, then write the policy against the body's fields:
aws sns set-subscription-attributes --subscription-arn "$SUBSCRIPTION_ARN" --attribute-name FilterPolicyScope --attribute-value MessageBody
aws sns set-subscription-attributes --subscription-arn "$SUBSCRIPTION_ARN" --attribute-name FilterPolicy --attribute-value '{"issue":["battery"]}'
Published with {"order":"A-2001","issue":"battery"} and {"order":"A-2002","issue":"screen"}, only A-2001 reached the battery queue. Attributes are still the lighter choice when you control the publisher: the filter doesn't depend on the body's structure, and the body can change without breaking it.
Jake: "So the post office can sort by the note, or open the box and look inside."
Ethan: "It can do both. Sorting by the note is quicker and never breaks when the box changes."
Read attributes in a Lambda function
When Lambda reads from an SQS queue, each record in the event carries its attributes under messageAttributes, with a lowercase stringValue and dataType. Notice the different capitalization from the SQS API:
import json
def lambda_handler(event, context):
for record in event["Records"]:
attrs = {name: a.get("stringValue") for name, a in record.get("messageAttributes", {}).items()}
order = json.loads(record["body"])
print(f"order {order['order']} type={attrs.get('OrderType')} priority={attrs.get('Priority')}")
return {"processed": len(event["Records"])}
order A-1042 type=repair priority=2
Lambda requests the attributes for you, so there's no MessageAttributeNames to set. If the queue is fed by SNS without raw delivery, the attributes are still inside record["body"], exactly as in the Python consumer above.
The rules and limits
| Rule | Detail |
|---|---|
| How many | Up to 10 attributes per message |
| Size | Names, types and values all count toward the 1 MiB maximum message size, together with the body |
| Name characters | Letters, digits, underscore, hyphen and period |
| Name length | Up to 256 characters |
| Name restrictions | Can't start with AWS. or Amazon. in any capitalization; can't start or end with a period, or have two periods in a row; must be unique in the message |
| Case | Names are case-sensitive: OrderType and ordertype are different attributes |
| Types | String, Number, Binary, optionally with a custom suffix such as Number.int |
| Empty values | The name, type and value must not be empty, and neither may the body |
Break one of these on SendMessage, such as an eleventh attribute or an empty value, and SQS rejects the whole message, so check the error in your producer's logs. The 1 MiB limit is new enough that some queues and publishers still enforce the older 256 KiB; our message size guide covers that error.
Troubleshooting by symptom
MessageAttributes is missing or null in ReceiveMessage
Add MessageAttributeNames=["All"] (or --message-attribute-names All). Without it, SQS returns the body only.
I set AttributeNames and still don't get my attributes
AttributeNames is for SQS system attributes. Custom attributes need MessageAttributeNames.
Messages from SNS have no SQS attributes
Raw message delivery is off, so the attributes are inside the body's JSON under MessageAttributes. Turn raw delivery on, or read them from the body.
Some SNS messages never reach the queue
With raw delivery on, SNS discards messages with more than 10 attributes for that subscription. Reduce them to 10 or turn raw delivery off.
My Lambda code can't find StringValue
In Lambda events the keys are lowercase: messageAttributes, stringValue, dataType.
SNS filter policy matches nothing
By default it filters on message attributes. If the field is in the JSON body, set FilterPolicyScope to MessageBody.
SQS message attributes: frequently asked questions
What are SQS message attributes?
Up to 10 typed name and value pairs sent alongside a message body, used for metadata such as type, priority or a trace ID, which consumers can read without parsing the body.
What is the difference between SQS message attributes and the message body?
The body is the data your consumer processes. Attributes are labels about it, used for routing, filtering and tracing. The body is always returned; attributes only when requested.
Why are my SQS message attributes empty?
ReceiveMessage only returns message attributes you request. Pass MessageAttributeNames with All, .* or the names you want.
How many message attributes can an SQS message have?
Up to 10. With more, SQS rejects the message, and SNS discards it for SQS subscriptions that use raw message delivery.
Do message attributes count toward the SQS message size?
Yes. Every part of every attribute, name, type and value, counts toward the 1 MiB maximum message size along with the body.
What data types do SQS message attributes support?
String, Number and Binary, each optionally with a custom label after a period, such as Number.int or String.code. Numbers are sent as text in StringValue.
How do I read SQS message attributes in Lambda?
Each record has messageAttributes; read record["messageAttributes"]["Name"]["stringValue"]. Lambda requests the attributes for you.
How do I get SNS message attributes in SQS?
Turn on raw message delivery for the subscription, and they arrive as SQS message attributes. Without it, they're in the body's JSON under MessageAttributes, each with a Type and Value.
What is SNS raw message delivery?
A subscription setting that delivers your published message as-is, without SNS's JSON envelope. For SQS subscriptions, your attributes then become SQS message attributes, up to 10.
Can SNS filter on the message body?
Yes. Set the subscription's FilterPolicyScope to MessageBody. The default, MessageAttributes, filters on attributes.
What is the difference between AttributeNames and MessageAttributeNames?
AttributeNames (or MessageSystemAttributeNames) returns SQS system attributes such as ApproximateReceiveCount. MessageAttributeNames returns the custom attributes you sent.
Are SQS message attribute names case-sensitive?
Yes. OrderType and ordertype are different attributes. Names can't start with AWS. or Amazon. in any capitalization.
Can an SQS message attribute be empty?
No. The name, type and value must all be non-empty, and so must the message body.
Should I put metadata in SQS attributes or the body?
Attributes, if something needs it to route, filter or trace the message without reading the body. Anything the consumer needs to do its work belongs in the body.
How do I send SQS message attributes with boto3?
Pass MessageAttributes to send_message, for example {"OrderType": {"DataType": "String", "StringValue": "repair"}}.
What does .* mean in MessageAttributeNames?
All attributes, the same as All. A prefix such as Order.* returns every attribute whose name starts with Order.
Ethan's fix was one parameter in the worker's receive call, and the tags appeared at once. A week later, when the website started publishing through an SNS topic so the accounting system could get a copy, he turned on raw delivery for the worker's subscription, kept every message to four attributes, and gave the consumer the two-shape code from this page, just in case. Screen repairs now land on one bench and batteries on the other, sorted by a sticky note nobody has to open the box to read.
📌 If you keep one line from this page
SQS message attributes only come back when you ask with MessageAttributeNames, and from SNS they're only real attributes with raw delivery, where more than 10 gets the message dropped.
Body = the data · attributes = labels (up to 10) · receive with All · Lambda = messageAttributes / stringValue.
Revision note. Written October 13, 2026, for every label that was there all along. "Ask, and it shall be given you," goes the old saying; with SQS attributes, that's the whole fix.