Guide · AWS SNS

SNS Subscription Filter Policies for MCP Servers — Attribute Routing, String Match, Numeric Operators

SNS subscription filter policies let each subscriber declare which messages it wants to receive — SNS evaluates the policy at delivery time and drops non-matching messages before they reach the subscriber. Without filtering, every subscriber receives every message published to the topic, which means a queue or Lambda wired up to receive only critical alerts still processes thousands of healthy-probe events per day. Filter policies eliminate that unnecessary processing: publish once to a central status topic and route degraded/unhealthy events to an alerting queue, healthy events to an archiving queue, and latency_ms > 5000 events to a SLO dashboard — all from one topic, with zero application code in the router.

TL;DR

Attach a FilterPolicy JSON object to a subscription. Keys are message attribute names (or JSON body field paths with FilterPolicyScope: MessageBody). Values are arrays of match conditions. Multiple keys = AND. Multiple array values per key = OR. The 5 KB limit on filter policy JSON is per subscription, not per topic. Messages that match no subscription's filter policy are still counted as delivered (and billed) at the topic level but produce no downstream delivery.

Filter policy scope: message attributes vs message body

SNS supports two filter policy scopes, set per subscription via the FilterPolicyScope attribute:

ScopeFilterPolicyScope valueWhat is matched
Message attributes (default)MessageAttributesThe MessageAttributes map passed in the Publish call. Faster evaluation; attributes are not part of the body, so no JSON parse needed.
Message bodyMessageBodyThe JSON body of the published message. Keys in the filter policy must match JSON field paths. Top-level fields only — nested objects need to be expressed as dot-path strings (e.g., "endpoint.region").

For MCP monitoring, prefer message attributes: they are set explicitly at publish time, avoid coupling the filter policy to your JSON schema, and do not require SNS to parse the message body on every delivery attempt. Reserve MessageBody scope for legacy publishers you cannot modify to add attributes.

String operators

String operators apply to String-typed message attributes or string-valued JSON body fields.

OperatorFilter policy JSONMatches
Exact match["unhealthy"]Attribute value is exactly "unhealthy"
OR (multiple values)["degraded", "unhealthy"]Attribute value is "degraded" or "unhealthy"
Prefix match[{"prefix": "us-east"}]Attribute value starts with us-east
Suffix match[{"suffix": "-prod"}]Attribute value ends with -prod
Anything-but (exclusion)[{"anything-but": ["healthy"]}]Attribute value is anything except "healthy"
Exists (attribute present)[{"exists": true}]The named attribute exists in the message, regardless of value
Not exists[{"exists": false}]The named attribute is absent from the message
Equals-ignore-case[{"equals-ignore-case": "UNHEALTHY"}]Case-insensitive exact match
// Publish with message attributes for filter policy evaluation
await snsClient.send(new PublishCommand({
  TopicArn: process.env.STATUS_TOPIC_ARN,
  Message: JSON.stringify({ endpointId, status, latencyMs, region }),
  MessageAttributes: {
    status:   { DataType: "String", StringValue: status },         // "healthy"|"degraded"|"unhealthy"
    region:   { DataType: "String", StringValue: region },         // "us-east-1", "eu-west-1", ...
    severity: { DataType: "String", StringValue: getSeverity(status, latencyMs) }, // "low"|"medium"|"high"|"critical"
    latencyMs: { DataType: "Number", StringValue: String(latencyMs) },
  },
}));

Numeric operators

Numeric operators apply to Number-typed message attributes. The attribute DataType must be "Number"; string attributes with numeric content are not evaluated as numbers.

OperatorFilter policy JSONMatches
Equal[{"numeric": ["=", 200]}]Value equals 200
Not equal[{"numeric": ["!=", 200]}]Value is not 200
Greater than[{"numeric": [">", 5000]}]Value > 5000 (latency alert)
Greater than or equal[{"numeric": [">=", 5000]}]Value >= 5000
Less than[{"numeric": ["<", 100]}]Value < 100
Range (between)[{"numeric": [">", 1000, "<", 5000]}]1000 < value < 5000

Numeric operators are useful for latency-tier routing: send latencyMs > 5000 events to a SLO-breach queue, latencyMs between 1000 and 5000 to a degraded-performance queue, and latencyMs < 1000 to a healthy archive queue — all from a single publish call.

AND and OR semantics

Understanding AND vs OR is critical to writing correct filter policies:

// Filter policy: (status is "degraded" OR "unhealthy") AND (region starts with "eu-")
// This subscription only receives alerts from European endpoints with non-healthy status
{
  "status": ["degraded", "unhealthy"],
  "region": [{ "prefix": "eu-" }]
}
// Filter policy: latencyMs > 3000 AND severity is "high" OR "critical"
// Routes high-latency critical alerts to an ops pager queue
{
  "latencyMs": [{ "numeric": [">", 3000] }],
  "severity": ["high", "critical"]
}

Combining numeric and string conditions: You can mix string and numeric operators within a single filter policy. The AND rule applies across all key types. A message with latencyMs = 4500 (matches > 3000) but severity = "low" (does not match ["high", "critical"]) will not be delivered to this subscription.

CDK: subscriptions with filter policies

import * as sns from "aws-cdk-lib/aws-sns";
import * as sqs from "aws-cdk-lib/aws-sqs";
import * as subscriptions from "aws-cdk-lib/aws-sns-subscriptions";

declare const statusTopic: sns.Topic;

// Queue 1: critical alerts only (status unhealthy OR degraded AND severity critical)
const criticalAlertQueue = new sqs.Queue(this, "CriticalAlertQueue");
statusTopic.addSubscription(
  new subscriptions.SqsSubscription(criticalAlertQueue, {
    rawMessageDelivery: true,
    filterPolicy: {
      status: sns.SubscriptionFilter.stringFilter({
        allowlist: ["degraded", "unhealthy"],
      }),
      severity: sns.SubscriptionFilter.stringFilter({
        allowlist: ["high", "critical"],
      }),
    },
  })
);

// Queue 2: SLO latency breach (latencyMs > 5000, any status)
const sloBreachQueue = new sqs.Queue(this, "SloBreachQueue");
statusTopic.addSubscription(
  new subscriptions.SqsSubscription(sloBreachQueue, {
    rawMessageDelivery: true,
    filterPolicy: {
      latencyMs: sns.SubscriptionFilter.numericFilter({
        greaterThan: 5000,
      }),
    },
  })
);

// Queue 3: EU region all events (for regional dashboards)
const euRegionQueue = new sqs.Queue(this, "EuRegionQueue");
statusTopic.addSubscription(
  new subscriptions.SqsSubscription(euRegionQueue, {
    rawMessageDelivery: true,
    filterPolicyWithMessageBody: {
      region: sns.FilterOrPolicy.filter(
        sns.SubscriptionFilter.stringFilter({ matchPrefixes: ["eu-"] })
      ),
    },
  })
);

Filter policy size limit and evaluation performance

ConstraintValueNotes
Max filter policy size per subscription5 KB (message attributes scope)Complex policies with many attribute conditions can hit this limit
Max filter policy size per subscription (MessageBody scope)5 KBSame limit applies
Max values in an array for one attribute150Limits OR fan-out per attribute
Max attributes in one filter policy5Max 5 AND conditions in a single policy
Filter evaluation costNo additional chargeMessages that fail the filter are not delivered but ARE counted for topic message throughput pricing

Billing note: SNS charges per message published to the topic, not per delivery. If you publish 10,000 healthy-probe events per day to a topic with a filter policy that excludes healthy events, those 10,000 messages are still billed as published. Only use SNS filtering to reduce downstream processing cost — not to reduce SNS publishing cost.

Common failure modes

SymptomCauseFix
Subscriber receives no messages despite matching intentMessage attribute DataType mismatch — attribute published as String but filter uses numeric operatorEnsure numeric filter attributes use DataType: "Number" at publish time
All messages pass filter regardless of policyFilterPolicyScope not set — policy uses body field paths but scope defaults to MessageAttributesSet FilterPolicyScope: MessageBody on the subscription via SetSubscriptionAttributes
Policy update takes effect slowlySNS propagates filter policy changes asynchronously; up to 15 minutes for all endpoints to pick up new policyWait 15 minutes after SetSubscriptionAttributes before testing; do not assume immediate effect
Policy exceeds 5 KBToo many attribute conditions or large allowlistsSplit into multiple subscriptions; or move high-cardinality routing to a Lambda subscriber that applies custom logic
Anything-but filter lets through unexpected valuesAnything-but with a string value (not array) is treated as exact-match exclusion onlyUse array syntax: {"anything-but": ["healthy", "recovering"]}