Guide · AWS SNS
SNS FIFO Topics for MCP Servers — Ordered Event Delivery, MessageGroupId, Deduplication
Amazon SNS FIFO (First-In, First-Out) topics guarantee ordered delivery and exactly-once processing within each message group. Standard SNS topics deliver at-least-once with best-effort ordering — acceptable for independent probe results, but problematic when downstream consumers need to process MCP endpoint status transitions in sequence (e.g., a healthy → degraded → unhealthy state machine). FIFO topics enforce strict ordering per MessageGroupId and eliminate duplicate processing via MessageDeduplicationId. The tradeoff: FIFO topics can only deliver to SQS FIFO queues or Lambda subscribers, and throughput is capped at 300 messages/second per message group rather than the standard topic's 30,000 msg/s.
TL;DR
Create a topic with the .fifo name suffix and FifoTopic: true. Assign a MessageGroupId per MCP endpoint (or per logical group) to scope ordering. Enable ContentBasedDeduplication on the topic or supply an explicit MessageDeduplicationId per publish call. Subscribers must be SQS FIFO queues or Lambda functions — HTTP/S, email, and SMS cannot subscribe to FIFO topics. Throughput is 300 published messages/second per message group (hard limit, not configurable).
FIFO vs Standard SNS topics
| Property | Standard Topic | FIFO Topic |
|---|---|---|
| Message ordering | Best-effort (not guaranteed) | Strict per-MessageGroupId |
| Delivery semantics | At-least-once (duplicates possible) | Exactly-once per deduplication window |
| Throughput | 30,000 msg/s per topic | 300 msg/s per message group; 3,000 msg/s if batched |
| Allowed subscriber types | SQS, Lambda, HTTP/S, email, SMS, mobile push | SQS FIFO, Lambda only |
| Deduplication window | None | 5 minutes |
| Topic ARN format | arn:aws:sns:region:account:topic-name | arn:aws:sns:region:account:topic-name.fifo |
| Message filtering | Supported | Supported (same filter policy syntax) |
| Cross-region delivery | Supported | Not supported — same-region subscribers only |
MessageGroupId: ordering scope for MCP endpoints
Every message published to a FIFO topic requires a MessageGroupId. Messages with the same GroupId are delivered in the exact order they were published. Choose the granularity of your GroupId to match the ordering requirement:
| Granularity | MessageGroupId example | Effect |
|---|---|---|
| Per-endpoint (recommended for MCP monitoring) | endpoint-{endpointId} | Status transitions for one MCP server are ordered; independent servers do not block each other |
| Per-region | region-us-east-1 | All events in a region are ordered; a slow consumer on one region blocks all others in that region |
| Global single group | global | All events ordered globally; maximum blocking risk; only 300 msg/s total |
Blocking within a group: If a consumer fails to process message N, the FIFO queue holds all subsequent messages in the same group until N is either processed or moved to the DLQ. For MCP monitoring workloads, per-endpoint groups contain blast radius to a single endpoint — one flaky endpoint does not block status updates for thousands of others.
// Publish with per-endpoint MessageGroupId
const client = new SNSClient({ region: "us-east-1" });
await client.send(new PublishCommand({
TopicArn: "arn:aws:sns:us-east-1:123456789012:mcp-status.fifo",
MessageGroupId: `endpoint-${endpointId}`, // scopes ordering per endpoint
MessageDeduplicationId: `${endpointId}-${probeRunId}`, // explicit dedup ID
Message: JSON.stringify({
endpointId,
status: "degraded",
probeRunId,
probeAt: new Date().toISOString(),
latencyMs: 4820,
}),
MessageAttributes: {
status: { DataType: "String", StringValue: "degraded" },
region: { DataType: "String", StringValue: "us-east-1" },
},
}));
Deduplication: content-based vs explicit MessageDeduplicationId
FIFO topics deduplicate within a 5-minute sliding window. A message is considered a duplicate if a message with the same MessageDeduplicationId (or identical SHA-256 body hash, with content-based dedup enabled) was published in the previous 5 minutes. Duplicates are silently dropped — no error is returned to the publisher.
| Method | How it works | When to use |
|---|---|---|
Explicit MessageDeduplicationId | Publisher provides a unique string per logical event; same string = duplicate | When the same logical event can be published multiple times by different code paths (retry logic, at-least-once publisher) |
Content-based dedup (ContentBasedDeduplication: true) | SNS hashes the message body; identical body within 5 min = duplicate | When the message body itself encodes uniqueness (e.g., includes a probeRunId in the JSON) |
Pitfall: If ContentBasedDeduplication is enabled on the topic and two genuinely different events happen to have identical bodies within 5 minutes (e.g., two consecutive "healthy" probes with the same JSON payload), only the first is delivered. Include a timestamp or probeRunId in the body to ensure uniqueness, or use explicit MessageDeduplicationId keyed on {endpointId}-{probeRunId}.
CDK: FIFO topic with SQS FIFO subscriber
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";
import { Duration, RemovalPolicy } from "aws-cdk-lib";
// FIFO topic — name must end in .fifo
const statusTopic = new sns.Topic(this, "McpStatusFifo", {
topicName: "mcp-status.fifo",
fifo: true,
contentBasedDeduplication: false, // use explicit MessageDeduplicationId
});
// DLQ for failed deliveries (also FIFO to preserve order in DLQ)
const statusDlq = new sqs.Queue(this, "McpStatusDlq", {
queueName: "mcp-status-dlq.fifo",
fifo: true,
retentionPeriod: Duration.days(14),
});
// Main consumer queue (FIFO — required for FIFO topic subscribers)
const statusQueue = new sqs.Queue(this, "McpStatusQueue", {
queueName: "mcp-status.fifo",
fifo: true,
contentBasedDeduplication: false,
visibilityTimeout: Duration.seconds(30),
deadLetterQueue: {
queue: statusDlq,
maxReceiveCount: 3,
},
});
// Subscribe FIFO queue to FIFO topic
statusTopic.addSubscription(
new subscriptions.SqsSubscription(statusQueue, {
rawMessageDelivery: true, // skip SNS envelope — consumer receives raw JSON
filterPolicy: {
// only route degraded/unhealthy events to this queue
status: sns.SubscriptionFilter.stringFilter({
allowlist: ["degraded", "unhealthy"],
}),
},
})
);
Raw message delivery note: With rawMessageDelivery: true, the SQS consumer receives your original JSON body directly. Without it, the body is wrapped in an SNS envelope JSON object with fields like Type, MessageId, TopicArn, and Message (your original body as a string). Enable raw delivery when the consumer is purpose-built for this topic and you own both sides of the integration.
Throughput limits and batching
| Limit | Value | Notes |
|---|---|---|
| Published msg/s per message group (non-batched) | 300 | Hard limit; cannot be increased via Service Quotas |
| Published msg/s per message group (PublishBatch) | 3,000 | Use PublishBatchCommand with up to 10 messages per call |
| Max message size | 256 KB | Same as standard SNS |
| Deduplication window | 5 minutes | Cannot be adjusted |
| Message groups per topic | Unlimited | Each group is independently ordered and throttled |
| Max subscribers per FIFO topic | 100 | Soft limit; raisable via Support |
For MCP monitoring at scale (thousands of endpoints), the per-group 300 msg/s limit is not a bottleneck because each endpoint is its own group. The aggregate topic throughput scales with the number of distinct message groups in use.
Monitoring FIFO delivery with AliveMCP
FIFO topic delivery failures surface differently than standard topic failures. Because FIFO queues block on message-group head failures, a single unprocessable message can silently delay all subsequent status updates for an endpoint. AliveMCP detects this pattern indirectly: if the probe result timestamp in your status page diverges from real probe time by more than the expected processing lag, the consumer is likely blocked on a DLQ item.
CloudWatch metrics to monitor on a FIFO SNS/SQS pipeline:
# Messages waiting in main queue — should be near-zero under normal load
aws cloudwatch get-metric-statistics \
--namespace AWS/SQS \
--metric-name ApproximateNumberOfMessagesVisible \
--dimensions Name=QueueName,Value=mcp-status.fifo \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 60 --statistics Maximum
# DLQ depth — non-zero means a message group is blocked
aws cloudwatch get-metric-statistics \
--namespace AWS/SQS \
--metric-name ApproximateNumberOfMessagesVisible \
--dimensions Name=QueueName,Value=mcp-status-dlq.fifo \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 60 --statistics Maximum
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
InvalidParameter: Invalid parameter: Topic on subscribe | Trying to subscribe an HTTP/S endpoint or email address to a FIFO topic | Only SQS FIFO queues and Lambda functions can subscribe to FIFO topics |
| Publish returns success but consumer never receives message | Duplicate deduplication ID within 5-minute window — message silently dropped | Include a per-invocation unique token (probeRunId, UUID) in the dedup ID |
| Consumer stuck; messages for all endpoints in group stall | One message in a group failed processing and hit maxReceiveCount; moved to DLQ, but DLQ is not being consumed | Set up a DLQ alarm on ApproximateNumberOfMessagesVisible > 0; add DLQ consumer or manual redrive process |
KMSDisabledException on publish | FIFO topic uses CMK encryption and the key was disabled | Re-enable the CMK in KMS; check key rotation schedule |
| Cross-region subscriber not receiving messages | FIFO topics do not support cross-region delivery | Use EventBridge cross-region event bus replication, then fan-out to regional FIFO topics |
| Messages arrive out of order despite FIFO topic | Multiple message groups mixed into a single consumer Lambda with concurrency > 1 | Set Lambda reserved concurrency to 1 per logical group, or process by MessageGroupId in the consumer |