Amazon SQS FIFO Error Handling: Dead Letter Queues and Message Ordering in .NET
When processing messages from Amazon SQS FIFO queues, exceptions can disrupt message ordering and block subsequent messages. Let's explore how visibility timeouts, message retention periods, and Dead Letter Queues help manage failed messages while maintaining order guarantees.
Amazon SQS FIFO queues guarantee message ordering, but what happens when message processing fails?
When a message throws an exception during processing, it impacts not just that message, but also all subsequent messages in the same group.
Understanding how to handle these failures is critical for building reliable applications.
In this post, let's explore:
- How exceptions affect message ordering in FIFO queues
- How visibility timeouts control message redelivery
- Using message retention periods to prevent stuck messages
- Setting up Dead Letter Queues to handle persistent failures
- Sequential vs parallel processing considerations
Thanks to AWS for sponsoring this article in my .NET on AWS Series.
Setting Up the Demo Application
The demo application builds on the FIFO queue examples from previous posts in this series. If you're new to SQS FIFO queues, I recommend checking those out first.
Amazon SQS FIFO Deduplication: Prevent Duplicate Message Processing in .NET

Our setup consists of:
- ASP.NET Core API → Publishes weather forecast messages to the FIFO queue
- Console Application → Processes messages from the queue
To simulate processing errors, the consumer checks the loop number and throws an exception for even-numbered messages:
var receiveMessageRequest = new ReceiveMessageRequest{QueueUrl = queueUrl,MaxNumberOfMessages = 2};var response = await sqsClient.ReceiveMessageAsync(receiveMessageRequest);foreach (var message in response.Messages){var forecast = JsonSerializer.Deserialize<WeatherForecast>(message.Body);Console.WriteLine($"Processing message: Loop {forecast.LoopNumber}");// Simulate processing error on even numbersif (forecast.LoopNumber % 2 == 0){Console.WriteLine($"Processing error on Loop {forecast.LoopNumber}");throw new Exception("Simulated processing error");}// Delete message after successful processingawait sqsClient.DeleteMessageAsync(queueUrl, message.ReceiptHandle);}
When we publish four messages (loop numbers 1-4), the consumer processes message 1 successfully, but throws an exception on message 2.
What Happens When Message Processing Fails
When an exception occurs during message processing and we don't delete the message, SQS has no way of knowing the processing failed.
The message remains in the queue but becomes temporarily invisible to consumers. After some time, the message gets delivered again.
In the console output, you can see message 2 gets delivered repeatedly with exactly 30 seconds between each attempt:
2025-11-05 10:44:05 - Processing message: Loop 22025-11-05 10:44:05 - Processing error on Loop 22025-11-05 10:44:35 - Processing message: Loop 22025-11-05 10:44:35 - Processing error on Loop 22025-11-05 10:45:05 - Processing message: Loop 22025-11-05 10:45:05 - Processing error on Loop 2
Meanwhile, messages 3 and 4 remain waiting in the queue. They won't be delivered until message 2 is successfully processed or removed.
This is SQS FIFO enforcing message ordering — messages are only delivered in sequence.
Amazon SQS For the .NET Developer: How to Easily Get Started

Understanding Visibility Timeout
The 30-second delay between message redeliveries is controlled by the visibility timeout setting on the queue.
When a consumer receives a message from SQS, the message remains in the queue but becomes temporarily invisible to other consumers. This invisibility is controlled by the visibility timeout.
Navigate to your FIFO queue in the AWS Console and check the Details section. You'll see the default visibility timeout is 30 seconds.

Since we threw an exception without deleting the message, SQS assumes the consumer is still processing it. After the 30-second timeout expires, SQS releases the message back to the queue, making it available for redelivery.
Adjusting Visibility Timeout
Let's reduce the visibility timeout to see the impact. Navigate to your queue, click Edit, and change the visibility timeout from 30 seconds to 5 seconds.
After saving, you don't need to restart the consumer. The change takes effect immediately on the queue side.
Switch back to the console application, and you'll see the redelivery interval has decreased:
2025-11-05 10:46:35 - Processing message: Loop 22025-11-05 10:46:40 - Processing message: Loop 22025-11-05 10:46:45 - Processing message: Loop 2
The message now reappears every 5 seconds instead of 30.
The visibility timeout should match your typical message processing time.
If it's too short, messages might be redelivered to different consumers even though they're still being processed. This is especially important when running multiple consumer instances.
The AWS documentation on visibility timeout provides detailed guidance on choosing appropriate values.
Message Groups and Parallel Processing
Remember that FIFO queues enforce ordering per message group, not across the entire queue.
If you publish messages to different message groups, those groups are processed independently:
var sendMessageRequest = new SendMessageRequest{QueueUrl = queueUrl,MessageBody = JsonSerializer.Serialize(forecast),MessageGroupId = forecast.City, // Different cities = different groupsMessageDeduplicationId = Guid.NewGuid().ToString()};
If message 2 from the "Brisbane" group throws an error, it only blocks other messages in the Brisbane group. Messages in the "Melbourne" group continue processing normally.
Sequential vs Parallel Processing Within Amazon SQS Consumer
When we receive messages from SQS, we can receive up to 10 messages in a single call (or whatever MaxNumberOfMessages is set to).
In our example, we set MaxNumberOfMessages = 2, so SQS might deliver both message 2 and message 3 together:
var response = await sqsClient.ReceiveMessageAsync(receiveMessageRequest);// response.Messages could contain: [Loop 2, Loop 3]
In our current implementation, we process these messages sequentially using a foreach loop. This maintains ordering within our consumer.
However, you could also parallelize the processing:
await Parallel.ForEachAsync(response.Messages, async (message, ct) =>{// Process messages in parallelawait ProcessMessage(message, ct);});
Important: If you parallelize processing, you lose ordering guarantees within the consumer, even though SQS delivers messages in order.
Whether to process messages sequentially or in parallel depends on your business requirements:
- Sequential processing → Maintains strict ordering within each message group
- Parallel processing → Higher throughput but ordering is not guaranteed
If ordering is critical, process messages from the same group sequentially. You can still parallelize processing across different message groups.
Message Retention Period in Amazon SQS
By default, SQS retains messages for 4 days. If a message isn't deleted within this period, SQS automatically removes it from the queue.
Let's adjust this setting to see what happens. Navigate to your queue and click Edit. Change the message retention period from 4 days to 1 minute (the minimum allowed value).
The message expiration is based on the original enqueue timestamp, so our stuck message 2 is already well past 1 minute.
Save the setting and wait. After a short delay, check the consumer logs—message 2 stops being delivered. It's been deleted from the queue.
Message Retention and Impact on Subsequent Messages in SQS FIFO Queue
Now let's check what happened to messages 3 and 4. Navigate to your queue in the AWS Console and click Send and receive messages → Poll for messages.
The queue shows zero messages available.
Messages 3 and 4 were also deleted, even though they never encountered an error.
This is how FIFO queues handle retention periods. When a message expires due to retention period, SQS also deletes all subsequent messages in that group because it can no longer enforce the ordering guarantee.
Let's change the retention period back to a more reasonable value like 1 day to prevent accidental message loss.
Setting Up a Dead Letter Queue For Amazon SQS FIFO Queue
Rather than letting messages expire and disappear, we can set up a Dead Letter Queue (DLQ) to capture messages that fail processing repeatedly.
A Dead Letter Queue is a separate queue where SQS moves messages after they've been received a specified number of times without being successfully processed/deleted.
First, create a new FIFO queue to serve as the Dead Letter Queue. In the AWS Console, create a queue named weather-forecast-dlq.fifo (or similar).
The Dead Letter Queue must also be a FIFO queue when working with FIFO queues.
Navigate to your main queue, click Edit, and scroll to the Dead-letter queue section. Click Enabled and:
- Select the DLQ you just created (
weather-forecast-dlq.fifo) - Set Maximum receives to
3(or however many times as required)

This configuration means: if a message is received 3 times without being deleted, move it to the Dead Letter Queue.
Amazon SQS FIFO DLQ in Action
Let's publish messages again. With our consumer running, send four messages (loop numbers 1-4).
The consumer processes message 1 successfully, then repeatedly processes message 2:
Processing message: Loop 1Processing message: Loop 2 - Processing errorProcessing message: Loop 2 - Processing error (2nd time)Processing message: Loop 2 - Processing error (3rd time)Processing message: Loop 3Processing message: Loop 4 - Processing errorProcessing message: Loop 4 - Processing error (2nd time)Processing message: Loop 4 - Processing error (3rd time)
After the third failed attempt, message 2 is moved to the DLQ. The queue then delivers message 3, which processes successfully. Message 4 fails three times and is also moved to the DLQ.
Navigate to your Dead Letter Queue in the AWS Console and poll for messages. You'll see messages 2 and 4 sitting in the DLQ.
Processing Messages After DLQ
With the Dead Letter Queue configured, messages that can't be processed don't block the queue forever. Once moved to the DLQ:
- The remaining messages in the group continue processing
- You can inspect failed messages to understand what went wrong
- After fixing the underlying issue, you can replay messages back into the main queue
- You can also delete messages from the DLQ if they're no longer needed
Amazon SQL FIFO DLQ and Message Ordering
Notice that with the DLQ enabled, message 3 was processed even though message 2 failed. This means using a DLQ changes the ordering guarantee.
If strict ordering is critical for your business case, you have two options:
- Don't use a DLQ and instead implement application-level retry logic
- Implement ordering checks in your application to verify previous messages were processed before processing subsequent ones:
foreach (var message in response.Messages){var forecast = JsonSerializer.Deserialize<WeatherForecast>(message.Body);// Check if previous message was processedif (!await WasPreviousMessageProcessed(forecast.LoopNumber - 1)){// Throw exception to move all subsequent messages to DLQ togetherthrow new Exception("Previous message not processed - maintaining order");}// Process message...}
This ensures that if message 2 fails, messages 3 and 4 also fail immediately and all get moved to the DLQ together, maintaining the ordering semantics your application requires.
Whether you prioritize strict ordering or prefer to handle failures gracefully depends entirely on your application's business requirements.
👉 Full source code is available here