Guide · AWS SQS FIFO
SQS FIFO Message Deduplication for MCP Servers — MessageDeduplicationId vs ContentBasedDeduplication
SQS FIFO queues deduplicate messages within a 5-minute window: if two messages with the same deduplication token arrive at the same queue within that window, the second is silently discarded. This is critical for MCP monitoring pipelines where probe workers may retry a failed send — without deduplication you could process the same probe result twice, double-count an alert, or flip a verdict back to a stale state. The two mechanisms are MessageDeduplicationId (you supply an explicit token per message) and ContentBasedDeduplication (SQS hashes the body). Choosing the wrong one for your payload shape is one of the most common sources of silent message loss in SQS FIFO queues: a payload that contains a timestamp or any non-deterministic field will never match on content hash, making ContentBasedDeduplication a no-op — but two healthy-probe payloads with the same body content will always match, dropping one silently.
TL;DR
Enable ContentBasedDeduplication on the queue only if your message bodies are fully deterministic (no timestamps, no UUIDs, no per-invocation trace IDs). For probe-result payloads — which always include a probeRunId or timestamp — use explicit MessageDeduplicationId keyed on {endpointId}-{probeRunId}. The deduplication window is exactly 5 minutes and cannot be changed. Messages with the same ID sent after the window are treated as new. If you omit both mechanisms on a FIFO queue, SQS returns a InvalidParameterValue error.
How the deduplication window works
When a message arrives at a FIFO queue, SQS records the deduplication token and the arrival time. Any subsequent message with the same token received within 5 minutes of the first is silently accepted (SQS returns success) but not enqueued — the consumer never sees it. After 5 minutes the token expires, and a message with the same token is treated as a new, distinct message.
Two important consequences for MCP probe pipelines:
- Retry safety: if your probe worker crashes after sending to SQS but before recording the send locally, a retry within 5 minutes is safe — the duplicate is dropped. After 5 minutes, a retry enqueues a second copy. Size your retry timeout accordingly.
- Idempotent reprocessing: replaying a probe result from a dead-letter queue that was originally sent within the last 5 minutes will be deduplicated. Wait for the window to expire before replaying DLQ messages that should re-enter the pipeline.
ContentBasedDeduplication: when it works and when it silently breaks
When ContentBasedDeduplication is enabled on a queue, SQS computes a SHA-256 hash of the raw message body and uses that as the deduplication token. You do not supply a MessageDeduplicationId — SQS derives it automatically.
This is safe only when your message bodies are fully deterministic for logically identical events:
| Payload pattern | ContentBasedDeduplication safe? | Why |
|---|---|---|
Pure command (no timestamps, no UUIDs): {"action":"ping","endpoint":"..."} | Yes | Same logical command always produces the same hash |
Probe result with timestamp field | No | Each send has a unique timestamp — hash never matches, deduplication never fires |
Probe result with probeRunId (UUID) | No | Each probe run has a unique ID — same broken behaviour as timestamp |
| Aggregated hourly health summary (deterministic for given hour + endpoint) | Yes, with care | Same hour + endpoint always produces the same body, but a body with even one float field may differ across architectures |
JSON with key ordering not guaranteed (e.g., JSON.stringify on an object whose properties were inserted in different orders) | No | Same logical payload serialises to different byte sequences → different hashes |
The failure mode is silent: SQS accepts the message, returns HTTP 200, but never enqueues a second copy when you expect it to. Your consumer processes only the first copy of a legitimately distinct probe result, and you may never notice — the DLQ stays empty, no errors are thrown, messages simply vanish.
MessageDeduplicationId: format best practices
An explicit MessageDeduplicationId is a string up to 128 characters, alphanumeric and !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~. It must be unique per distinct logical event within the 5-minute window.
Recommended formats for MCP monitoring pipelines:
// Probe result: one result per endpoint per run — {endpointId}-{probeRunId}
const deduplicationId = `${endpointId}-${probeRunId}`;
// Example: "server-abc123-run-9f4e2b"
// Alert dispatch: one alert per endpoint per incident — {endpointId}-{incidentId}
const deduplicationId = `alert-${endpointId}-${incidentId}`;
// Example: "alert-server-abc123-inc-20261029T120000Z"
// Verdict-minute aggregation: one aggregated verdict per endpoint per minute
const minute = new Date().toISOString().slice(0, 16).replace(':', ''); // "2026-10-29T1200"
const deduplicationId = `verdict-${endpointId}-${minute}`;
// Example: "verdict-server-abc123-2026-10-29T1200"
Key rules:
- Do not use raw UUIDs as the sole component — they are unique per invocation, so two retries of the same logical event get different IDs and both enqueue.
- Derive the ID from the logical identity of the event (endpoint + run + type), not from the message content or send timestamp.
- Keep the ID stable across retries of the same event — if your worker retries on timeout, the ID must be pre-computed before the first send and stored locally, not re-derived from a new UUID.
- For consumer-side retry from a DLQ, preserve the original
MessageDeduplicationIdfrom the DLQ message attributes when re-enqueuing to the source queue after the 5-minute window.
Enabling deduplication in CDK
import { Queue, QueueEncryption, DeduplicationScope, FifoThroughputLimit } from 'aws-cdk-lib/aws-sqs';
// Option A — content-based (only for deterministic payloads)
const probeCommandQueue = new Queue(this, 'ProbeCommandQueue', {
queueName: 'mcp-probe-commands.fifo',
fifo: true,
contentBasedDeduplication: true, // SQS derives token from SHA-256(body)
visibilityTimeout: Duration.seconds(30),
encryption: QueueEncryption.KMS_MANAGED,
});
// Option B — explicit MessageDeduplicationId (recommended for probe results)
const probeResultQueue = new Queue(this, 'ProbeResultQueue', {
queueName: 'mcp-probe-results.fifo',
fifo: true,
contentBasedDeduplication: false, // caller must supply MessageDeduplicationId
visibilityTimeout: Duration.seconds(30),
encryption: QueueEncryption.KMS_MANAGED,
});
// Sending with explicit ID (AWS SDK v3)
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
const sqs = new SQSClient({});
await sqs.send(new SendMessageCommand({
QueueUrl: probeResultQueueUrl,
MessageGroupId: endpointId, // per-endpoint ordering
MessageDeduplicationId: `${endpointId}-${probeRunId}`,
MessageBody: JSON.stringify(probeResult),
}));
Deduplication scope in high-throughput FIFO mode
Standard FIFO queues deduplicate across the entire queue — the token is global. High-throughput FIFO queues (enabled by setting DeduplicationScope=messageGroup and FifoThroughputLimit=perMessageGroupId) scope deduplication to the message group: the same MessageDeduplicationId can be safely reused across different MessageGroupId values.
const highThroughputQueue = new Queue(this, 'HighThroughputProbeQueue', {
queueName: 'mcp-probe-ht.fifo',
fifo: true,
contentBasedDeduplication: false,
});
// Enable high-throughput FIFO via escape hatch (CDK L1)
const cfnQueue = highThroughputQueue.node.defaultChild as CfnQueue;
cfnQueue.deduplicationScope = 'messageGroup';
cfnQueue.fifoThroughputLimit = 'perMessageGroupId';
For AliveMCP's per-endpoint probe pipeline, high-throughput FIFO makes sense only if you have many endpoints each generating probe results at high frequency. With message-group scoped deduplication, a MessageDeduplicationId of run-{probeRunId} (short form, without endpoint prefix) is sufficient — two different endpoints can use the same short form without collision, because their MessageGroupId values differ.
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
| Messages silently lost after producer retry | Retry occurs within 5-minute window with the same deduplication ID — second message deduplicated | Expected and correct. Ensure retry logic computes the same ID as the original send; if it generates a new UUID, second copy will enqueue |
| Same probe result processed twice despite FIFO queue | Two sends of the same event use different MessageDeduplicationId values (e.g., new UUID generated per retry) | Pre-compute deduplication ID before first send, store it, reuse on retry |
InvalidParameterValue on send | Queue has contentBasedDeduplication: false and no MessageDeduplicationId supplied | Always supply MessageDeduplicationId when content-based deduplication is off |
| DLQ replay re-deduplicates and message never reaches consumer | Replaying DLQ message within 5 minutes of original send | Wait 5 minutes before replaying; or use a different deduplication ID on replay if the original event semantics have changed |
| ContentBasedDeduplication drops distinct probe results | Two probes for the same endpoint produce identical JSON body (e.g., both returned healthy with same latency and the body has no per-run fields) | Add a probeRunId field to the body, or switch to explicit MessageDeduplicationId |