30.4 SNS Message Filtering: Attribute-Based Subscription Filters
Right, so you’ve set up your SNS topic. Messages are flying. But now you want to be a bit more… discriminating. You don’t want every single subscriber to get every single message. Maybe the order-created service only cares about orders from the ecommerce team, and the fraud-detection service only wants orders over $10,000. Broadcasting everything to everyone is wasteful, noisy, and frankly, a bit rude.
This is where SNS message filtering comes in. It’s the feature that lets your subscribers raise their hand and say, “I’ll take messages, but only the ones that look like this.”
Think of it like this: without filtering, it’s a megaphone in a crowded room. With filtering, you’re handing out personalized earpieces. It’s more efficient, cheaper (for your downstream services), and a whole lot more elegant.
How It Actually Works: Attributes Are Everything
Here’s the crucial part you must internalize: SNS filtering works exclusively on message attributes. It does not filter based on the message body. I’ll wait for the collective groan to sub.
Yes, it’s a bit absurd. You have this perfectly good JSON body sitting right there, but SNS pretends it doesn’t exist for filtering purposes. The designers decided that filtering should be a contract on the envelope (the attributes), not the letter inside (the body). Their reasoning, which I’ll admit has some merit, is that it forces a separation of concerns. The attributes are for routing, the body is for content. It prevents you from writing horrifically slow filter policies that have to parse a JSON string for every single message.
So, if you want to filter, you must publish your filterable data as message attributes. Let’s look at a proper publish command.
import boto3
import json
sns = boto3.client('sns')
message_attributes = {
'team': {
'DataType': 'String',
'StringValue': 'ecommerce'
},
'order_value': {
'DataType': 'Number',
'StringValue': '125.99' # Notice it's a string representing a number
},
'environment': {
'DataType': 'String',
'StringValue': 'production'
}
}
message_body = json.dumps({"orderId": "12345", "items": [...]})
response = sns.publish(
TopicArn='arn:aws:sns:us-east-1:123456789012:orders',
Message=message_body,
MessageAttributes=message_attributes
)
If you forget to add environment as an attribute and only have it in the body, your filter on environment will never, ever match. Don’t make that mistake. I certainly have.
Crafting the Filter Policy Itself
The filter policy is a JSON document you attach to a subscription. It’s a set of rules where each key is the name of an attribute and each value is the condition the attribute must meet.
The simplest filter is an exact match. This subscription only wants messages from the ecommerce team.
{
"team": ["ecommerce"]
}
But it gets more powerful. You can use numeric comparisons (=, >, >=, <, <=), prefix matching, and whitelist multiple values. This policy is for our paranoid fraud detection system that wants expensive orders in prod or staging.
{
"environment": ["production", "staging"],
"order_value": [{"numeric": [">=", 10000]}]
}
You can also use the anything-but operator to blacklist values and prefix to match the start of a string. The logic between different attributes is AND (both environment AND order_value must match). The logic within a single attribute’s values is OR (environment can be “production” OR “staging”).
The “Null” Pitfall and How to Avoid It
Here’s a classic “gotcha” that will bite you. What happens if a message is published without the environment attribute at all? By default, the filter "environment": ["production"] will reject it, which is probably what you want.
But what if you want to accept messages that have environment set to production OR are missing the attribute entirely? You have to explicitly allow for null. AWS represents this with a special keyword.
{
"environment": ["production", null]
}
This policy will match messages where the environment attribute is “production” OR where the environment attribute is completely absent. This is incredibly important for designing robust systems that can handle both new and old message formats. Always think about what happens when an attribute is missing.
Best Practices: Don’t Shoot Yourself in the Foot
- Keep it Simple, Seriously: Filter policies are evaluated for every message against every subscription. If you write a wildly complex policy, you’re not just slowing down one thing, you’re slowing down the entire topic’s fanout. Use filter policies for routing, not for complex business logic. That logic belongs in the consumer.
- Version Your Attributes: Adding a new attribute? Consider something like
version: ["2023-12-05"]in your filter. This allows you to roll out new message formats and gradually migrate subscribers by updating their filter policy, instead of a risky big-bang cutover. - Test with the CLI: Don’t guess. Use the
sns test-messageCLI command to test your filter policy against a sample message before you deploy it. It’s a lifesaver.aws sns test-message-filter \ --filter-policy file://policy.json \ --message-attributes '{"environment":{"DataType":"String","StringValue":"production"}}' - It’s Not a Security Mechanism: Rely on IAM policies for security. Filter policies are a routing feature. A malicious actor with permissions to publish to your topic could easily add attributes that match your filters and land messages where they shouldn’t. Trust is established at the IAM level, not the filter level.
Used wisely, attribute-based filtering transforms SNS from a blunt instrument into a precision tool. It forces you to think explicitly about your message contract, which is almost always a good thing. Just remember to put the stuff you want to filter on in the envelope.