SNS subscriptions — PendingConfirmation, FilterPolicy scope, protocol census
A subscription that never confirmed is a dead endpoint that still lists.
<!-- hal:authoritative:yaml -->
A subscription that never confirmed is not a quiet subscriber. It is a dead endpoint that still shows up in ListSubscriptionsByTopic.
§I. Frame
Amazon SNS is the regional pub/sub broker. Bootcamp SAP sns.md: a publisher sends to a topic; subscribers receive; filters can narrow what each subscriber sees; fan-out is one topic with many SQS (and other) subscribers.
SQS DLQ census (09-22) and Service Bus DLQ census (10-01) already taught side queues after failed receives. Today the referent is the subscription row itself: whether it finished confirmation, whether it carries a FilterPolicy, which FilterPolicyScope it uses, and which protocol owns the endpoint.
The problem for today: list every SNS topic in the account/region, walk every subscription, and print the ones that are pending, unfiltered, body-scoped, or otherwise worth an owner review. End with a protocol census.
§II. The Pending Gate
The Pending Gate (named technique). Treat PendingConfirmation as a hard stop before you trust delivery metrics or filter behavior.
Three facts from the Subscribe / GetSubscriptionAttributes docs:
| Fact | Source | Cost of ignoring it |
|---|---|---|
PendingConfirmation=true (or ARN string PendingConfirmation) | GetSubscriptionAttributes / ListSubscriptionsByTopic | HTTPS, email, SMS, and some cross-account SQS paths never receive until ConfirmSubscription |
| Confirmation token lasts two days | Subscribe API | After expiry you must Subscribe again; the old token is dead |
| Filter policy changes can take up to 15 minutes | Filter policy docs (eventual consistency) | A freshly attached FilterPolicy can look "broken" in the first quarter hour |
HTTP(S), email, email-json, and SMS need an endpoint owner to confirm. SQS and Lambda in the same account usually auto-confirm when IAM and resource policies allow the subscribe. Do not invent a confirm column for SQS from folklore; read the attribute.
§III. FilterPolicy and scope
FilterPolicy is a JSON object on the subscription. Property names map to lists of allowed values. SNS ignores message fields that the policy does not name.
FilterPolicyScope:
MessageAttributes(default): match publisher-supplied message attributes.MessageBody: match JSON fields in the message body.
If a FilterPolicy exists and scope is omitted, scope defaults to MessageAttributes. EventBridge, SES, and RDS event shapes often need MessageBody because the useful keys live in the body, not in attributes. Putting EventBridge detail-type under MessageAttributes scope is a silent miss, not an API error.
Raw message delivery (RawMessageDelivery=true) strips the SNS JSON envelope for SQS and HTTP(S). Flag it when present so the consumer contract is visible in the census.
§IV. Shape the census
Inputs: default AWS credential chain (AWS_PROFILE, env keys, or instance role) and a region. IAM: sns:ListTopics, sns:ListSubscriptionsByTopic, sns:GetSubscriptionAttributes.
Algorithm:
list_topicswith pagination.- Per topic,
list_subscriptions_by_topicwith pagination. - Per subscription, if the ARN is not the literal
PendingConfirmation, callget_subscription_attributes. - Flag when pending, when FilterPolicy is absent/empty, when scope is
MessageBody, or when raw delivery is on. - Tally protocols into a BTreeMap summary.
Quiet rows (confirmed, active MessageAttributes filter, not raw) stay silent. The print is the exception list, then totals.
§V. cargo script + aws-sdk-sns
sns-subscription-census.rs (frontmatter abbreviated):
use aws_config::BehaviorVersion;
use aws_sdk_sns::Client;
let cfg = aws_config::defaults(BehaviorVersion::latest()).load().await;
let client = Client::new(&cfg);
let page = client.list_topics().send().await?;
for topic in page.topics() {
let topic_arn = topic.topic_arn().unwrap();
let subs = client
.list_subscriptions_by_topic()
.topic_arn(topic_arn)
.send()
.await?;
for sub in subs.subscriptions() {
// pending ARN short-circuit, else get_subscription_attributes
}
}
Four mechanics:
- **
list_topicsthenlist_subscriptions_by_topic.** Account-widelist_subscriptionsexists; topic-scoped listing keeps the ARN column tied to the topic you care about. - Pending ARN short-circuit. When
subscription_arnis the stringPendingConfirmation, skip GetSubscriptionAttributes; there is no real ARN yet. - Attributes map is stringly typed.
attributes()returnsOption<&HashMap<String, String>>; keys arrive as strings. - Paginator tokens. Both list calls return
next_token. Loop until none.
What was checked, and what was not. A scratch crate matching the script frontmatter passed cargo check on the lab Mac rustc/cargo 1.99.0. The lab Mac has no usable AWS credentials for a live ListTopics. Sample output below is illustrative:
arn:aws:sns:us-east-1:111122223333:orders https https://hooks.example/sns pending
arn:aws:sns:us-east-1:111122223333:orders sqs arn:aws:sqs:us-east-1:111122223333:orders-all no_filter
arn:aws:sns:us-east-1:111122223333:orders sqs arn:aws:sqs:us-east-1:111122223333:orders-gold filter_scope=MessageBody
arn:aws:sns:us-east-1:111122223333:alerts lambda arn:aws:lambda:us-east-1:111122223333:function:pager no_filter
topics=2 subscriptions=5 flagged=4 pending=1 filtered=1 body_scope=1
protocol https 1
protocol lambda 1
protocol sqs 3
§VI. Wrap it with Nix
sns-subscription-census.nix:
{ pkgs ? import <nixpkgs> { } }:
pkgs.writers.writeBashBin "sns-subscription-census" ''
exec cargo +nightly -Zscript ${./sns-subscription-census.rs} "$@"
''
The wrapper pins the script path into the store. The toolchain still comes from rustup on the host (cargo +nightly).
§VII. What not to do
- Calling a pending HTTPS subscription "healthy" because CloudWatch has no delivery failures yet.
- Attaching FilterPolicy before ConfirmSubscription on confirmable protocols, then blaming SNS when nothing arrives.
- Assuming MessageAttributes scope when the publisher only puts keys in the JSON body.
- Treating SNS FilterPolicy as an SQS redrive policy. Different object, different failure mode.
- Granting
sns:*to the census role because List* "might need more."
§VIII. Close instruction
Run the census in an account you own once credentials exist. For every pending row, find the ConfirmSubscription token owner or delete the subscription and recreate it. For every no_filter row on a shared topic, decide whether open delivery is intentional. For every filter_scope=MessageBody row, verify the publisher body schema still matches the policy keys. File the protocol tally next to the topic inventory.
Related
- Dev: enums and match for SNS filter policy shapes (same trio)
- Cert: SAP SNS fan-out, filter policies, protocols (same trio)
- Prior Cloud Ops: SQS DLQ and redrive census
- Prior Cloud Ops: Service Bus DLQ census
the study notes/certified-aws-solutions-architect-professional/09-containers-and-serverless/sns.md