Skip to main content
SEMastery

Complete Guide to Amazon SQS and Amazon SNS with MassTransit

A friendly, step-by-step guide to messaging in .NET using Amazon SQS, Amazon SNS, and MassTransit — queues, topics, consumers, retries, and dead-letter handling.

13 min readUpdated October 7, 2025

A post office and a notice board

Imagine your family runs a small sweet shop.

When a customer places an order, one cousin needs to pack the box. You do not want two cousins packing the same box, and you do not want the box forgotten. So you drop a little slip into a letterbox. Whoever is free picks the slip, packs that one order, and throws the slip away. Each slip is handled exactly once. That letterbox is Amazon SQS — a queue.

But sometimes news needs to reach many people at once. Say the shop got a new batch of fresh laddoos. You want to tell the billing cousin, the delivery cousin, and the cousin who updates the website — all of them. Phoning each one is slow. Instead you pin one notice on a board. Everyone who cares walks past and reads it. That notice board is Amazon SNS — a topic.

MassTransit is the helpful manager who sets up the letterboxes and the notice board, writes the slips neatly, hands each slip to the right cousin, and tries again if a cousin drops one. You just say what happened. The manager does the messy work.

Let us learn how SQS, SNS, and MassTransit fit together in .NET, step by step.

Why use messaging at all?

When one service calls another directly over HTTP, both must be awake at the exact same moment. If the second one is slow or down, the first one is stuck waiting.

Direct calls make every service depend on every other service being awake right now.

This is called temporal coupling — everything must be up together. Messaging breaks the chain. The Order Service writes "an order was placed" and moves on. The other services read the news when they are ready. Nobody waits on anybody.

SQS and SNS — who does what?

These two AWS services are simple on their own. The magic is using them together.

FeatureAmazon SQSAmazon SNS
ShapeQueue (a line of messages)Topic (a notice board)
Who reads a messageExactly one consumerEvery subscriber gets a copy
Good forCommands and one-worker tasksEvents many services care about
Message waits?Yes, until read and deletedNo, it is pushed out immediately
Common pairingThe mailbox each service ownsFans an event out to many mailboxes

The classic pattern is SNS in front of SQS. You publish one event to an SNS topic. SNS copies that event into each subscribed SQS queue. Each service then reads from its own queue at its own pace. You get fan-out from SNS and safe, durable storage from SQS.

One publish to SNS fans out into several SQS queues, one per interested service.

Where MassTransit fits

You could call the AWS SDK directly. But then you must create topics, create queues, wire up subscriptions, set permissions, serialize messages to JSON, poll for new messages, delete handled ones, and retry on errors. That is a lot of careful code.

MassTransit does all of that for you. You define a message (a plain C# class), a consumer (a class that handles it), and a little configuration. When the bus starts, MassTransit creates the SNS topics, the SQS queues, and the subscriptions automatically.

From your code to AWS, the MassTransit way

Define message
Write consumer
Configure bus
Bus creates topics & queues
Messages flow

Steps

1

Define message

A plain C# record like OrderPlaced.

2

Write consumer

A class that does the work.

3

Configure bus

Tell MassTransit to use Amazon SQS.

4

Bus creates topics & queues

Topics, queues, subscriptions made for you.

5

Messages flow

Publish and consume with a few lines.

You write messages and consumers. MassTransit handles the AWS plumbing.

Step 1 — Install the packages

You need MassTransit and its Amazon SQS transport. Install them with the .NET CLI.

// Run these in your project folder:
// dotnet add package MassTransit
// dotnet add package MassTransit.AmazonSQS
 
// A note on licensing:
// From MassTransit v9 onward, MassTransit is a commercial,
// source-available product. There is a free evaluation license
// for local development, and small companies (under 1 million USD
// gross annual revenue) may qualify for a free license.
// Check the current terms before shipping to production.

Step 2 — Define a message

A message is just a plain class or record. Keep it simple. It should describe what happened, not how to handle it. Past-tense names work well for events.

namespace SweetShop.Contracts;
 
// An event: "this already happened."
public record OrderPlaced
{
    public Guid OrderId { get; init; }
    public string CustomerEmail { get; init; } = string.Empty;
    public decimal Total { get; init; }
    public DateTime PlacedAtUtc { get; init; }
}

Put your message contracts in a shared project so both the sender and the receivers can reference the exact same type. MassTransit matches messages to topics using the .NET type name, so both sides must agree.

Step 3 — Write a consumer

A consumer is a class that handles one message type. It implements IConsumer<T>. MassTransit calls your Consume method whenever a matching message arrives in the queue.

using MassTransit;
using SweetShop.Contracts;
 
public class SendWelcomeEmailConsumer : IConsumer<OrderPlaced>
{
    private readonly ILogger<SendWelcomeEmailConsumer> _logger;
 
    public SendWelcomeEmailConsumer(ILogger<SendWelcomeEmailConsumer> logger)
    {
        _logger = logger;
    }
 
    public async Task Consume(ConsumeContext<OrderPlaced> context)
    {
        OrderPlaced order = context.Message;
 
        _logger.LogInformation(
            "Sending email for order {OrderId} to {Email}",
            order.OrderId, order.CustomerEmail);
 
        // Do the real work here: send the email, call an API, etc.
        await Task.CompletedTask;
    }
}

If this method throws, MassTransit does not delete the message. It becomes visible again later and is retried. That safety net is one of the biggest reasons to use a message broker.

Step 4 — Configure the bus

Now wire MassTransit to Amazon SQS in your Program.cs. This is where you give your AWS region and credentials, and register your consumers.

using MassTransit;
 
builder.Services.AddMassTransit(x =>
{
    // Register every consumer in this assembly.
    x.AddConsumer<SendWelcomeEmailConsumer>();
 
    x.UsingAmazonSqs((context, cfg) =>
    {
        cfg.Host("eu-west-1", h =>
        {
            h.AccessKey(builder.Configuration["AWS:AccessKey"]);
            h.SecretKey(builder.Configuration["AWS:SecretKey"]);
        });
 
        // Let MassTransit create queues, topics, and subscriptions,
        // then attach the right consumers to each receive endpoint.
        cfg.ConfigureEndpoints(context);
    });
});

When the app starts, MassTransit looks at your consumers, works out which messages they handle, and builds the matching SNS topics and SQS queues in your AWS account. You did not touch the AWS console once.

What happens the moment the bus starts up.

Step 5 — Publish and send

There are two ways to put a message into the system, and the difference matters.

  • Publish sends an event to a topic. Every subscriber gets a copy. Use this for "something happened" events like OrderPlaced.
  • Send delivers a command to one specific queue. Only that queue's consumer handles it. Use this for "please do this one thing" commands.
// PUBLISH: fan an event out to everyone who cares.
public class OrderService
{
    private readonly IPublishEndpoint _publishEndpoint;
 
    public OrderService(IPublishEndpoint publishEndpoint)
    {
        _publishEndpoint = publishEndpoint;
    }
 
    public async Task PlaceOrderAsync(Guid orderId, string email, decimal total)
    {
        // ... save the order to the database first ...
 
        await _publishEndpoint.Publish(new OrderPlaced
        {
            OrderId = orderId,
            CustomerEmail = email,
            Total = total,
            PlacedAtUtc = DateTime.UtcNow
        });
    }
}

The publisher does not know who is listening. Tomorrow you can add a loyalty-points service that subscribes to OrderPlaced, and the Order Service never changes. That is the power of pub-sub.

OperationGoes toWho handles itUse it for
PublishAn SNS topicEvery subscriberEvents: "OrderPlaced", "UserRegistered"
SendOne SQS queueOne consumerCommands: "ShipOrder", "ChargeCard"

Handling failures: retries and dead letters

Things go wrong. A network blips. A database is briefly locked. MassTransit lets you retry before giving up. You add a retry policy to the consumer's endpoint.

cfg.ReceiveEndpoint("email-service", e =>
{
    // Try 3 more times, waiting 5 seconds between tries.
    e.UseMessageRetry(r => r.Interval(3, TimeSpan.FromSeconds(5)));
 
    // Turn on the SQS redrive policy so poison messages
    // move to a dead-letter queue instead of looping forever.
    e.ConfigureConsumer<SendWelcomeEmailConsumer>(context);
});

If a message still fails after all retries, you do not want it stuck forever, blocking the queue. SQS supports a dead-letter queue (DLQ): a side mailbox where hopeless messages are moved. You can inspect them later, fix the bug, and replay them. MassTransit also has its own _error and _skipped queues for faulted and unhandled messages.

The life of a failing message

Arrives
Consumer throws
Retry a few times
Still failing
Dead-letter queue

Steps

1

Arrives

Message lands in the SQS queue.

2

Consumer throws

Something goes wrong while handling it.

3

Retry a few times

MassTransit waits and tries again.

4

Still failing

All retries used up.

5

Dead-letter queue

Moved aside for a human to inspect.

Retries first, then off to the dead-letter queue so the line keeps moving.
A message either succeeds, gets retried, or ends up dead-lettered.

Testing locally with LocalStack

You do not want to touch real AWS every time you run the app on your laptop. LocalStack is a tool that fakes AWS services on your own machine. MassTransit supports it with one method call.

cfg.Host("us-east-1", h =>
{
    // Point at LocalStack instead of real AWS.
    h.AccessKey("test");
    h.SecretKey("test");
    h.Config(new AmazonSimpleNotificationServiceConfig
    {
        ServiceURL = "http://localhost:4566"
    });
    h.Config(new AmazonSQSConfig
    {
        ServiceURL = "http://localhost:4566"
    });
});

Newer MassTransit versions also offer a LocalstackHost() helper that sets this up for you. Either way, your tests run fast, cost nothing, and never depend on a real AWS account.

Idempotency: handle the same message twice safely

Standard SQS promises at-least-once delivery. Most of the time a message arrives once. But if a consumer crashes after doing the work but before telling SQS it finished, the message comes back and is handled again.

So your consumers should be idempotent — handling the same message twice has the same result as handling it once. A common way is to remember which message IDs you have already processed, often using the Inbox Pattern. Before doing the work, check "have I seen this message ID before?" If yes, skip it.

public async Task Consume(ConsumeContext<OrderPlaced> context)
{
    Guid messageId = context.MessageId ?? Guid.Empty;
 
    // Inbox check: have we already handled this exact message?
    if (await _inbox.AlreadyProcessedAsync(messageId))
        return; // safe to ignore the duplicate
 
    // ... do the real work ...
 
    await _inbox.MarkProcessedAsync(messageId);
}

For strict ordering and built-in deduplication, AWS also offers FIFO queues and topics (their names end in .fifo). They keep messages in order and remove duplicates within a five-minute window, at the cost of lower throughput. Use them only when order truly matters, like financial steps that must run one after another.

Scoping topics and queues

There is only one SNS and SQS namespace per AWS account. If many services share the account, their names can clash. MassTransit lets you add a scope prefix so each service's topics and queues stay tidy and separate.

cfg.Host("eu-west-1", h =>
{
    h.AccessKey(accessKey);
    h.SecretKey(secretKey);
    h.Scope("sweet-shop", scopeTopics: true);
});

Now all topics and queues start with sweet-shop, so they will not collide with another team's OrderPlaced topic. This is a small setting that saves big headaches in shared accounts.

Putting it all together

Here is the full picture for our sweet shop, from a customer order to several services reacting.

End to end: one order, one publish, many happy consumers.

The Order Service does its own job, saves the order, and publishes once. Email and Shipping each get their own copy in their own queue and work at their own pace. If Shipping is briefly down, its messages wait safely in SQS until it wakes up. Nothing is lost.

Common mistakes to avoid

  • Publishing before saving. If you publish OrderPlaced but the database save then fails, you have lied to everyone. Save first, or use the Outbox Pattern so the save and the publish happen together.
  • Forgetting idempotency. At-least-once delivery means duplicates will happen one day. Plan for it from the start.
  • No dead-letter queue. Without a DLQ, one poison message can be retried forever and clog the line. Always set a redrive policy.
  • Sharing message types loosely. Keep contracts in a shared library so the sender and receiver use the exact same type name. MassTransit routes by type.
  • Heavy work inside Consume. Keep consumers quick. If the work is long, hand it off so the message can be acknowledged and the queue keeps moving.

Quick recap

  • Amazon SQS is a queue. Each message is read by exactly one consumer, then deleted.
  • Amazon SNS is a topic, like a notice board. A published message is copied to every subscriber.
  • The strong combo is SNS in front of SQS: publish once, fan out to many service queues.
  • MassTransit creates the topics, queues, and subscriptions for you and routes messages to consumer classes.
  • Use Publish for events ("something happened") and Send for commands ("do this one thing").
  • Add retries and a dead-letter queue so failures are handled gracefully, not lost or looped.
  • SQS is at-least-once, so make consumers idempotent, often with the Inbox Pattern.
  • Test locally with LocalStack so you never depend on real AWS while coding.
  • MassTransit v9+ is commercial and source-available — check the license before you ship.

References and further reading

Related Patterns