Hedronite · Ops Lesson · 01-Earth-DevOps / AWS SNS · Sun 2026-10-04

SNS subscriptions — PendingConfirmation, FilterPolicy scope, protocol census

A subscription that never confirmed is a dead endpoint that still lists.

Lesson Class: Ops (DevOps + AWS SNS subscription lifecycle)
Topic: T2 Ops scripting
Cloud Referent: PendingConfirmation · FilterPolicy · FilterPolicyScope · protocols
Automation: cargo script · aws-sdk-sns 1 · aws-config 1 · Nix writeBashBin
Paired Dev: Rust enums and match for SNS filter policy shapes
Paired Cert: AWS SAP SNS fan-out filter policies and protocols
The Pending Gate
Confirm before you trust delivery or filters.
Filter scope
MessageAttributes default; MessageBody for JSON bodies.
Protocol census
Tally http/https/email/sqs/lambda/firehose.
Pending is not quiet. It is undelivered.

<!-- 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:

FactSourceCost of ignoring it
PendingConfirmation=true (or ARN string PendingConfirmation)GetSubscriptionAttributes / ListSubscriptionsByTopicHTTPS, email, SMS, and some cross-account SQS paths never receive until ConfirmSubscription
Confirmation token lasts two daysSubscribe APIAfter expiry you must Subscribe again; the old token is dead
Filter policy changes can take up to 15 minutesFilter 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:

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:

  1. list_topics with pagination.
  2. Per topic, list_subscriptions_by_topic with pagination.
  3. Per subscription, if the ARN is not the literal PendingConfirmation, call get_subscription_attributes.
  4. Flag when pending, when FilterPolicy is absent/empty, when scope is MessageBody, or when raw delivery is on.
  5. 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:

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

  1. Calling a pending HTTPS subscription "healthy" because CloudWatch has no delivery failures yet.
  2. Attaching FilterPolicy before ConfirmSubscription on confirmable protocols, then blaming SNS when nothing arrives.
  3. Assuming MessageAttributes scope when the publisher only puts keys in the JSON body.
  4. Treating SNS FilterPolicy as an SQS redrive policy. Different object, different failure mode.
  5. 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