# Why SNS Silently Drops Your Messages and How to Catch It Before You Ship

> Source: <https://dev.to/siddharth_pandey_27/why-sns-silently-drops-your-messages-and-how-to-catch-it-before-you-ship-1fcm>
> Published: 2026-07-11 08:53:46+00:00

Your checkout service publishes an `OrderRefunded`

event to an SNS topic. The publish call returns a `MessageId`

. No exception, no retry, nothing in the dead-letter queue. Three days later a customer emails asking where their refund is, and you discover the refund Lambda was never invoked.

The message wasn't lost. It was filtered. One of the topic's subscriptions has a filter policy that requires an `eventType`

message attribute, and your publish call didn't include it. SNS did exactly what it was configured to do: it evaluated the filter, found no match, and skipped delivery for that subscription. From the publisher's side, everything looks like success.

This is one of the nastiest failure modes in event-driven AWS architectures, because every tool you'd normally reach for reports green. This post walks through why it happens, why neither your code review nor your AI coding assistant can catch it from source code alone, and how to make the contract visible at coding time instead of incident time.

SNS filter policies live on the *subscription*, not the topic. A subscriber says "only deliver messages where the `eventType`

attribute is one of these values":

```
{
  "eventType": ["order.refunded", "order.cancelled"]
}
```

For that subscription to ever receive anything, the publisher must include the attribute:

```
await sns.send(
  new PublishCommand({
    TopicArn: process.env.ORDER_EVENTS_TOPIC,
    Message: JSON.stringify(refund),
    MessageAttributes: {
      eventType: { DataType: 'String', StringValue: 'order.refunded' },
    },
  }),
);
```

Omit `MessageAttributes`

and the publish still succeeds — SNS accepts the message and returns a `MessageId`

regardless of whether any subscription matches. The message simply isn't delivered to the filtered subscription.

Three things make this failure quiet:

`NumberOfNotificationsFilteredOut`

CloudWatch metric, but unless you already suspect filtering, nobody looks at it — and by the time you do, you're debugging in production.`PublishCommand`

was called. The mock has no filter policy. The gap between your code and the subscription's configuration is exactly the part the test can't see.There is a second variant of the same trap: filter policies can also match against the message *body* (`FilterPolicyScope: MessageBody`

) instead of message attributes. Then the required keys must appear in your JSON payload itself. Same silence, different location.

Ask an AI coding assistant to "publish an order refunded event to the order-events topic" and it will produce a perfectly reasonable `PublishCommand`

call — topic ARN from an environment variable, `JSON.stringify`

on the payload, maybe a comment. What it will almost never produce is the `MessageAttributes`

block with exactly the keys your subscriptions filter on.

It can't. The filter policy is not in your repository. It's a JSON document attached to a subscription in your AWS account, set by whoever wired up the consumer — possibly you, six months ago. The assistant reads your source files, sees other publish calls (which may themselves be missing attributes), and generates code that matches the pattern it found. If the pattern is wrong, the wrongness propagates.

The manual fix is miserable: open the AWS console, navigate to the topic, open each subscription one by one, read each filter policy, and paste the required attribute names into your prompt. Every session. For every topic. This is the copy-paste loop that breaks flow — and if you skip it once, you ship the silent drop.

This is the problem [infrawise](https://github.com/Sidd27/infrawise) is built for: it extracts your live infrastructure into a graph and serves it to your AI assistant over MCP, so the assistant queries facts instead of guessing.

```
cd your-project
npx infrawise start --claude
```

That analyzes your AWS account and codebase, writes `.mcp.json`

, and opens Claude Code with 21 MCP tools connected. For SNS specifically, the extraction is read-only and direct: infrawise lists your topics, then for every confirmed subscription fetches its attributes, parses the `FilterPolicy`

JSON, and records which attribute keys the policy requires and whether the policy scope is `MessageAttributes`

or `MessageBody`

.

The result is exposed through a tool called `get_topic_details`

. For each topic it returns the subscription count, encryption status, and a `filterPolicies`

array:

```
{
  "name": "order-events",
  "provider": "aws",
  "subscriptionCount": 3,
  "encrypted": true,
  "filterPolicies": [
    {
      "subscriptionArn": "arn:aws:sns:...:order-events:a1b2...",
      "protocol": "sqs",
      "requiredAttributes": ["eventType"],
      "scope": "MessageAttributes"
    }
  ]
}
```

Now the workflow changes. When you ask your assistant to write a publish call, it calls `get_topic_details`

first, sees that a subscription on `order-events`

filters on `eventType`

, and writes the `MessageAttributes`

block into the code on the first attempt. The contract that used to live invisibly in a subscription's configuration is now part of the context every generated publish call is checked against.

The tool description itself tells the assistant when to use it — before writing any SNS publish code — so you don't have to remember to prompt for it. And because infrawise also scans your application code (an AST pass that recognizes `PublishCommand`

, `PublishBatchCommand`

, and SDK `publish`

calls and resolves their target topic ARNs), the graph knows which of your functions publish to which topics. Reviewing an existing publisher works the same way: the assistant can cross-reference what the function sends against what the topic's subscriptions require.

A few boundaries worth stating, because tools that read your AWS account should be explicit about them. Infrawise is read-only — the SNS extraction uses `GetTopicAttributes`

, `ListSubscriptionsByTopic`

, and `GetSubscriptionAttributes`

, nothing that writes. It never reads message contents, secret values, or parameter values. And the analysis is deterministic: AST parsing and API introspection, no LLM deciding what your infrastructure looks like. The LLM is only a consumer of the extracted context.

Silent message drops are a configuration-versus-code mismatch, and those never show up in the layer you're staring at. The filter policy is correct. The publish code is correct. Only the combination is broken, and the combination exists nowhere in your repository — until you put it there.

You can do that manually every session by reading subscriptions in the console, or you can have it extracted once and served to your assistant automatically. If you're on the second option: [GitHub](https://github.com/Sidd27/infrawise) · [npm](https://www.npmjs.com/package/infrawise) — `npx infrawise start --claude`

and ask your assistant what the `order-events`

topic requires.

`MessageId`

returned, no DLQ entry, no error. The redrive policy only covers delivery failures, and a filter mismatch isn't one.`FilterPolicyScope`

: policies can require message `get_topic_details`

gives your AI assistant that list automatically.`NumberOfNotificationsFilteredOut`

CloudWatch metric is your post-hoc signal, but the goal is to never need it — catch the missing attribute at coding time.
