AWS SQS FIFO Queues: Ordering, Deduplication, Message Groups, and Dead Letter Configuration
Master AWS SQS FIFO queues for ordered, exactly-once message delivery. Covers message group IDs for parallel ordering, deduplication IDs, dead letter queues, content-based deduplication, Terraform setup, and TypeScript consumer patterns.
Standard SQS queues are fast and nearly unlimited in throughput, but they don't guarantee message order and can deliver the same message more than once. FIFO (First-In, First-Out) queues guarantee order and exactly-once processing — at the cost of lower throughput (3,000 messages/second with batching) and a few implementation constraints that trip up teams who aren't familiar with them.
This guide covers FIFO queue design, message group IDs for parallel ordering, deduplication, dead letter configuration, and real consumer patterns.
Standard vs FIFO: When to Use Each
| Requirement | Standard Queue | FIFO Queue |
|---|---|---|
| Maximum throughput | ✅ Unlimited | ❌ 3,000/s (batched) |
| Message ordering | ❌ Best-effort | ✅ Strict per group |
| Exactly-once delivery | ❌ At-least-once | ✅ Yes |
| Use case | Notifications, fan-out | Payments, order processing, ledger events |
Terraform: FIFO Queue Setup
# terraform/sqs-fifo.tf
# Dead letter queue (must also be FIFO)
resource "aws_sqs_queue" "orders_dlq" {
name = "orders-dlq.fifo"
fifo_queue = true
content_based_deduplication = true # Hash of body as dedup ID
# DLQ messages expire after 14 days (max)
message_retention_seconds = 1209600
tags = { Environment = var.environment }
}
# Main FIFO queue
resource "aws_sqs_queue" "orders" {
name = "orders.fifo"
fifo_queue = true
content_based_deduplication = false # We'll provide explicit dedup IDs
# How long a consumer has to process a message before it reappears
# Set this LONGER than your expected processing time to avoid double delivery
visibility_timeout_seconds = 60
# How long messages wait before expiring (max 14 days)
message_retention_seconds = 86400 # 1 day for orders
# Long polling: reduce empty receives (saves cost)
receive_wait_time_seconds = 20
redrive_policy = jsonencode({
deadLetterTargetArn = aws_sqs_queue.orders_dlq.arn
maxReceiveCount = 5 # After 5 failed attempts, move to DLQ
})
tags = { Environment = var.environment }
}
# Allow ECS task / Lambda to send and receive
data "aws_iam_policy_document" "orders_queue_policy" {
statement {
effect = "Allow"
actions = ["sqs:SendMessage", "sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes", "sqs:ChangeMessageVisibility"]
resources = [aws_sqs_queue.orders.arn]
principals {
type = "AWS"
identifiers = [aws_iam_role.app.arn]
}
}
}
resource "aws_sqs_queue_policy" "orders" {
queue_url = aws_sqs_queue.orders.id
policy = data.aws_iam_policy_document.orders_queue_policy.json
}
output "orders_queue_url" { value = aws_sqs_queue.orders.id }
output "orders_dlq_url" { value = aws_sqs_queue.orders_dlq.id }
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
TypeScript: Sending Messages
// lib/queues/orders-queue.ts
import { SQSClient, SendMessageCommand, SendMessageBatchCommand } from "@aws-sdk/client-sqs";
const sqs = new SQSClient({ region: process.env.AWS_REGION! });
const QUEUE_URL = process.env.ORDERS_QUEUE_URL!;
export type OrderEvent =
| { type: "order.created"; orderId: string; userId: string; amount: number }
| { type: "order.paid"; orderId: string; paymentId: string }
| { type: "order.shipped"; orderId: string; trackingNumber: string }
| { type: "order.cancelled"; orderId: string; reason: string };
interface SendOrderEventParams {
event: OrderEvent;
// Message group = partition key for ordering
// All events for the same order use the same group ID
// → events for order-123 are always processed in order
// → events for different orders process in parallel
messageGroupId: string;
// Deduplication ID: SQS rejects duplicate sends within 5-minute window
// Use idempotency key or deterministic ID (event type + resource ID + timestamp)
deduplicationId: string;
}
export async function sendOrderEvent({
event,
messageGroupId,
deduplicationId,
}: SendOrderEventParams): Promise<string> {
const command = new SendMessageCommand({
QueueUrl: QUEUE_URL,
MessageBody: JSON.stringify(event),
MessageGroupId: messageGroupId,
MessageDeduplicationId: deduplicationId,
// Optional: delay message (not available for FIFO — use EventBridge Scheduler instead)
});
const response = await sqs.send(command);
return response.MessageId!;
}
// Helper: deterministic dedup ID
export function makeDeduplicationId(parts: string[]): string {
const { createHash } = require("crypto");
return createHash("sha256").update(parts.join(":")).digest("hex").slice(0, 128);
}
// Usage: send an order created event
async function handleOrderCreated(order: { id: string; userId: string; amount: number }) {
await sendOrderEvent({
event: {
type: "order.created",
orderId: order.id,
userId: order.userId,
amount: order.amount,
},
// Group ID = order ID: all events for this order are ordered
messageGroupId: `order-${order.id}`,
// Dedup ID = event type + order ID: safe to retry API calls
deduplicationId: makeDeduplicationId(["order.created", order.id]),
});
}
Batch Send
// Send up to 10 messages in a single API call (reduces cost)
export async function sendOrderEventBatch(
messages: Array<{ event: OrderEvent; messageGroupId: string; deduplicationId: string; id: string }>
): Promise<void> {
const command = new SendMessageBatchCommand({
QueueUrl: QUEUE_URL,
Entries: messages.map(({ event, messageGroupId, deduplicationId, id }) => ({
Id: id, // Unique within the batch (not the SQS message ID)
MessageBody: JSON.stringify(event),
MessageGroupId: messageGroupId,
MessageDeduplicationId: deduplicationId,
})),
});
const response = await sqs.send(command);
if (response.Failed && response.Failed.length > 0) {
throw new Error(
`Batch send partial failure: ${response.Failed.map((f) => `${f.Id}: ${f.Message}`).join(", ")}`
);
}
}
⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
TypeScript: Consumer (ECS / Lambda)
// workers/order-consumer.ts
import {
SQSClient,
ReceiveMessageCommand,
DeleteMessageCommand,
ChangeMessageVisibilityCommand,
} from "@aws-sdk/client-sqs";
import type { OrderEvent } from "@/lib/queues/orders-queue";
const sqs = new SQSClient({ region: process.env.AWS_REGION! });
const QUEUE_URL = process.env.ORDERS_QUEUE_URL!;
// Process a single SQS message
async function processMessage(message: {
body: string;
receiptHandle: string;
messageId: string;
attributes: Record<string, string>;
}): Promise<void> {
const event = JSON.parse(message.body) as OrderEvent;
const approximateReceiveCount = parseInt(message.attributes.ApproximateReceiveCount ?? "1");
console.log(`Processing ${event.type} (attempt ${approximateReceiveCount})`, {
messageId: message.messageId,
});
switch (event.type) {
case "order.created":
await handleOrderCreated(event);
break;
case "order.paid":
await handleOrderPaid(event);
break;
case "order.shipped":
await handleOrderShipped(event);
break;
case "order.cancelled":
await handleOrderCancelled(event);
break;
default:
console.warn("Unknown event type:", (event as any).type);
}
}
// Main polling loop (runs in ECS task)
async function startConsumer(): Promise<void> {
console.log("Order consumer started");
while (true) {
const command = new ReceiveMessageCommand({
QueueUrl: QUEUE_URL,
MaxNumberOfMessages: 10, // FIFO: max 10 in flight per group by default
WaitTimeSeconds: 20, // Long polling — avoids empty receive cost
AttributeNames: ["ApproximateReceiveCount", "MessageGroupId"],
});
const { Messages } = await sqs.send(command);
if (!Messages || Messages.length === 0) continue;
// Process messages sequentially within each group
// (FIFO delivers them in order; parallel across groups is safe)
for (const msg of Messages) {
const receiptHandle = msg.ReceiptHandle!;
try {
await processMessage({
body: msg.Body!,
receiptHandle,
messageId: msg.MessageId!,
attributes: msg.Attributes ?? {},
});
// Delete message only after successful processing
await sqs.send(new DeleteMessageCommand({
QueueUrl: QUEUE_URL,
ReceiptHandle: receiptHandle,
}));
} catch (err) {
console.error("Failed to process message:", err);
// Extend visibility timeout on transient errors
// (lets it retry faster than waiting for original timeout to expire)
const receiveCount = parseInt(msg.Attributes?.ApproximateReceiveCount ?? "1");
if (receiveCount < 5) {
const backoffSeconds = Math.min(30 * receiveCount, 120);
await sqs.send(new ChangeMessageVisibilityCommand({
QueueUrl: QUEUE_URL,
ReceiptHandle: receiptHandle,
VisibilityTimeout: backoffSeconds,
})).catch(() => {}); // Non-critical — let natural expiry handle it
}
// If receive count >= 5, SQS will automatically move to DLQ
}
}
}
}
startConsumer().catch((err) => {
console.error("Consumer crashed:", err);
process.exit(1);
});
Lambda Consumer (Event Source Mapping)
# Lambda FIFO consumer (scales to 1 concurrent instance per group)
resource "aws_lambda_event_source_mapping" "orders" {
event_source_arn = aws_sqs_queue.orders.arn
function_name = aws_lambda_function.order_processor.arn
batch_size = 10
# For FIFO: scaling_config not available — Lambda creates 1 concurrent invocation per group
# With 5 active order groups, Lambda runs 5 concurrent instances
}
// lambda/order-processor/index.ts
import type { SQSEvent, SQSRecord } from "aws-lambda";
export async function handler(event: SQSEvent): Promise<void> {
// Lambda FIFO: all records in a batch are from the SAME message group
// If any record fails, the entire batch is retried (respects order)
for (const record of event.Records) {
const orderEvent = JSON.parse(record.body);
await processOrderEvent(orderEvent);
// No need to delete — Lambda auto-deletes on success
}
}
// For partial batch failures (some records succeed, some fail):
// Return { batchItemFailures: [{ itemIdentifier: record.messageId }] }
// Only failed items go back to queue
DLQ Monitoring and Alerting
# Alert when DLQ has messages (means orders failed processing)
resource "aws_cloudwatch_metric_alarm" "orders_dlq" {
alarm_name = "orders-dlq-not-empty"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ApproximateNumberOfMessagesVisible"
namespace = "AWS/SQS"
period = 60
statistic = "Sum"
threshold = 0
alarm_description = "Order DLQ has messages — investigate failed order events"
dimensions = { QueueName = aws_sqs_queue.orders_dlq.name }
alarm_actions = [aws_sns_topic.alerts.arn]
ok_actions = [aws_sns_topic.alerts.arn]
}
Common Pitfalls
// ❌ PITFALL 1: Not using MessageGroupId correctly
// If all messages go to the same group, you get no parallelism
await sendMessage({ MessageGroupId: "all" }); // Everything sequential!
// ✅ Group by natural ordering unit
await sendMessage({ MessageGroupId: `order-${orderId}` }); // Order per customer
// ❌ PITFALL 2: Visibility timeout shorter than processing time
// Message reappears mid-processing → double processing
// VisibilityTimeout: 10 seconds, but processing takes 30 seconds → duplicate
// ✅ Set visibility timeout to (max processing time) × 2
// If processing takes up to 30s: set 60s visibility timeout
// ❌ PITFALL 3: Deduplication ID collision
// Reusing the same dedup ID for logically different messages → second message silently dropped
// Always include enough entropy: event type + resource ID + version or timestamp
// ❌ PITFALL 4: Consuming FIFO with multiple parallel consumers
// If you spin up 2 consumer threads polling the same queue, ordering within a group
// can be violated if both pick up messages from the same group
// ✅ SQS handles this automatically: only one consumer gets messages from a given group at a time
Cost and Throughput
| Queue Type | Throughput | Cost |
|---|---|---|
| Standard | Nearly unlimited | $0.40/million requests |
| FIFO | 3,000 msg/s (batching) | $0.50/million requests |
| FIFO high throughput mode | 30,000 msg/s | $0.50/million + $0.01/million for high-throughput requests |
At 1 million messages/day: ~$0.50/day for FIFO.
See Also
- AWS SQS and SNS Event Patterns
- AWS SQS Dead Letter Queue Patterns
- AWS EventBridge Event-Driven Architecture
- SaaS Webhook System with Delivery Guarantees
- Terraform State Management
Working With Viprasol
FIFO queues are the right tool for payment processing, order management, and any workflow where event order matters — but they require care around message groups, deduplication IDs, and visibility timeouts. Our team has built FIFO-based event pipelines for financial SaaS and e-commerce platforms, with DLQ monitoring, alerting, and reprocessing tooling.
What we deliver:
- Terraform: FIFO queue + DLQ + IAM policy + CloudWatch alarm
- TypeScript producer with deterministic deduplication IDs
- ECS long-poll consumer with exponential backoff on failure
- Lambda event source mapping with partial batch failure handling
- Per-messageGroup ordering strategy design (partition key selection)
Talk to our team about your event queue architecture →
Or explore our cloud infrastructure services.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.