Queues without overtime: C#, Azure Service Bus and AWS SQS in one resilient flow

An interactive C# 14, .NET 10, Azure Service Bus and Amazon SQS workshop: outbox, idempotency, Blob/S3 claim-check, DLQ, incident costs, an animated map and a quiz with an email report.

The full article is available below. Enable JavaScript to use quizzes and simulations.

LAB 01 · RESILIENT FLOW C# 14 · .NET 10 · Azure Service Bus · Amazon SQS
Article decisions 0/4 Work through decisions in each chapter.

Clear score

Friday, 16:47

A queue buys the team time to respond safely

A customer confirms an order. The API responds correctly, but the external billing system slows. A worker loses its connection just after completing the operation, so the message arrives again. If the design assumes exactly one delivery, Saturday morning begins with a duplicated effect and manual cleanup.

This is a workshop, not an invented client success story. We will build a flow that expects repetition, delays and partial failures, using a shared C# port, transport strategy, transactional outbox, idempotent consumer, retries, a dead-letter queue and claim-check for files in Azure Blob Storage or Amazon S3. It ends with a ten-question assessment and an individual email report.

01

Mental model

A queue is a controlled waiting room, not a magic accelerator

In a synchronous system, the user waits for every dependent step. In an asynchronous system, the API accepts intent, saves its own state and delegates execution. A temporary integration failure need not stop the whole journey. This flexibility has a cost: recognizing duplicates, resuming work, monitoring backlogs and resolving messages automation cannot repair.

01Accept

Save intent and give it a stable identifier.

02Decouple

Do not make users wait for an external system.

03Repeat

Retries are a normal design condition.

04Prove

Metrics, logs and receipts must demonstrate one effect.

A map to keep

From request to effect: one contract, two transports

flow active
01APIcommand + requestId
02DB + Outboxone COMMIT
03Dispatcherpublish after persistence
AAzure Service BusPeekLock · Sessions · DLQ
BAmazon SQSVisibility · FIFO · DLQ
04Consumer + Inboxidempotency
05Effectreceipt + telemetry
Retrybackoff + jitterDLQ
QR code linking to the mobile queue-flow map
OPEN ON YOUR PHONE

Scan the map and keep it as a review checklist

The code links directly to this animated map. Open it while designing a worker or reviewing an incident without searching for the chapter.

Open the map without scanning →
Decision 1/4

The API saved an order, but the broker is temporarily unavailable. What do you do?

Wait and retry publication inside the HTTP request.

Save state and outbox in one transaction; let the dispatcher resume sending.

Choose an approach that will not lose the accepted order.

Technology status at publication

New C# syntax cannot fix bad boundaries, but can express them more clearly

C# 14 ships with .NET 10, an LTS release. Extension members, null-conditional assignment and further span improvements improve ergonomics, but reliability still comes from contracts and data. In integration code, a message identifier, version, correlation, occurrence time and minimal consumer data matter more than syntactic shortcuts.

An important Azure migration

Microsoft announced the end of support for the older libraries WindowsAzure.ServiceBus and Microsoft.Azure.ServiceBus on 30 September 2026. New code should use Azure.Messaging.ServiceBus, while existing systems need a planned migration and acceptance tests.

AWS for .NET

AWS SDK for .NET V4 provides direct SQS APIs, while AWS Message Processing Framework for .NET can organize publishing and consumption. A framework does not remove the need for idempotency, correct visibility timeouts or DLQ management.

Choosing transport

Azure Service Bus and Amazon SQS solve related problems with different tools

NeedAzure Service BusAmazon SQSDesign decision
Basic deliveryPeekLock provides at-least-once semanticsStandard Queue provides at-least-once deliveryConsumers must tolerate repetition
Ordering within a groupSessions with SessionIdFIFO with MessageGroupIdGroup by aggregate, not the whole system
Publication duplicatesDuplicate detection using MessageIdFIFO deduplication using a deduplication IDAn additional layer, not an inbox replacement
Processing timeLock duration and renewalVisibility timeout and extensionSet above p95 and monitor overruns
Poison messagesBuilt-in dead-letter queueRedrive policy to a DLQAlert, owner, retry policy and replay procedure
Large dataBlob Storage + a key in the messageS3 + a key in the messageClaim-check, checksum and time-limited access

“Exactly once” should not be a wish in the design. A practical goal is that messages may be redelivered while the observable business effect occurs once.

02

C# contract

Define the message envelope before choosing the cloud

The domain should not import Azure or AWS SDK types. Define a small application contract; the transport adapter maps it to broker properties.

public sealed record WorkEnvelope<T>(
    Guid MessageId,
    Guid CorrelationId,
    string TenantId,
    int SchemaVersion,
    DateTimeOffset OccurredAt,
    T Payload);

public interface IWorkQueue
{
    Task PublishAsync<T>(
        WorkEnvelope<T> message,
        QueueRoute route,
        CancellationToken cancellationToken);
}

MessageId protects against repeated effects, CorrelationId connects traces between the API and worker, TenantId supports isolation, and SchemaVersion allows safe evolution. Keep secrets, full documents and unnecessary “just in case” data out of the envelope.

STRATEGY + ADAPTER

Transport selected by policy

public interface IQueueStrategy
{
    bool Supports(CloudBroker broker);
    Task PublishAsync<T>(
        WorkEnvelope<T> message,
        QueueRoute route,
        CancellationToken ct);
}

public sealed class QueueRouter(
    IEnumerable<IQueueStrategy> strategies)
{
    public Task PublishAsync<T>(
        CloudBroker broker,
        WorkEnvelope<T> message,
        QueueRoute route,
        CancellationToken ct)
        => strategies.Single(x => x.Supports(broker))
            .PublishAsync(message, route, ct);
}
POLICY, NOT SCATTERED CONDITIONS

When does a router make sense?

  • regions or tenants need different transports,
  • a migration between clouds is underway,
  • controlled failover is required,
  • tests need an in-memory transport.

If the system always uses one broker, one adapter is simpler. An abstraction should address actual variation rather than invent future complexity.

Decision 2/4

A message must carry an 18 MB PDF. What goes into the queue?

The entire file as Base64 so the worker has everything immediately.

Object key, checksum and metadata; put the file in Blob Storage or S3.

Choose the approach that keeps the contract small and secure.

Claim-check

Blob Storage or S3 carries the bulk. The queue carries intent

The API stores a file under an unpredictable key, computes SHA-256 and publishes a reference. A worker downloads it using managed identity or an IAM role, verifies the hash and streams processing. Public access is disabled; retention and encryption follow the data classification.

uploadBlob / S3objectKey + SHA-256Queueworker
03

Atomicity

Outbox closes the gap between COMMIT and publication

Rather than assume a distributed transaction between PostgreSQL and the broker, save the business change and publication record in one local transaction.

await using var transaction =
    await db.Database.BeginTransactionAsync(ct);

var order = WorkOrder.Accept(command.RequestId, command.CustomerId);
db.WorkOrders.Add(order);

db.OutboxMessages.Add(OutboxMessage.Create(
    messageId: command.RequestId,
    type: "work-order.accepted.v1",
    payload: serializer.Serialize(new WorkOrderAccepted(order.Id))));

await db.SaveChangesAsync(ct);
await transaction.CommitAsync(ct);

// Dispatcher odczyta OutboxMessages dopiero po COMMIT.

The dispatcher reads unprocessed records in batches, publishes and marks progress. Two dispatchers need concurrency control before claiming the same record. PostgreSQL can use row locks and SKIP LOCKED or an explicit lease state. Always measure the age of the oldest outbox entry: a record count alone does not tell you how long a customer has waited.

04

Idempotent consumption

The inbox identifies duplicates before irreversible effects

Service Bus PeekLock and SQS Standard can redeliver messages. Consumers check their receipts, execute effects and settle the message only after durable persistence.

public async Task HandleAsync(
    WorkEnvelope<WorkOrderAccepted> message,
    CancellationToken ct)
{
    if (await db.InboxReceipts.AnyAsync(
        x => x.MessageId == message.MessageId, ct))
        return;

    await using var tx = await db.Database.BeginTransactionAsync(ct);

    await pricing.CreateQuoteAsync(
        message.Payload.WorkOrderId,
        idempotencyKey: message.MessageId.ToString("N"),
        ct);

    db.InboxReceipts.Add(new InboxReceipt(message.MessageId));
    await db.SaveChangesAsync(ct);
    await tx.CommitAsync(ct);
}
Decision 3/4

A worker completed its operation but disconnected before Complete/Delete. The message returns. What should happen?

Recognize the receipt by MessageId and finish without repeating the effect.

Execute everything again because the broker deliberately redelivered it.

Consider what proves completion when the broker acknowledgement is lost.

The economics of failure

A queue saves money when it reduces the cost of manual intervention

This calculation is an illustration for your own numbers, not an ArcCloud Studio case-study result. Assume four monthly incidents, three people spending an average of 2.5 hours and a fully loaded hourly cost of PLN 180.

4 incidents×3 people×2.5 h×PLN 180= PLN 5,400 / month

Add lost concentration, roadmap delays and customer risk. Compare the total with broker, telemetry and automated-testing costs. The cheapest architecture has the lowest total cost of achieving a safe outcome, not merely the lowest cloud bill.

Architecture game

Arrange seven steps of safe processing

Click tiles in order. A wrong choice loses no points; it shows which responsibility is still missing.

Progress0/7

BrokerASB or SQS stores the message

CommandThe API accepts a stable requestId

Business effectexecution with an idempotency key

Dispatcherpublishes the saved outbox

SettlementComplete or Delete after a durable result

DB + Outboxstate and message in one COMMIT

Consumer + Inboxduplicate and correlation checks

Start with an intention carrying a stable identifier.

Start again

Decision 4/4

The DLQ contains 37 messages. What is the correct first response?

Immediately return all messages to the main queue.

Classify the cause, fix it and replay a small batch under control.

A DLQ is an operational queue, not a bin to empty with one click.

Incident simulator

Run a message through a timeout, retry and duplicate

This controlled sequence checks requestId, outbox, transport, inbox, business effect, receipt and settlement.

Run the sequence

APIOUTBOXBROKERRETRYINBOXEFFECT

Lab ready. Start the controlled sequence.

Production runbook

What to monitor before the after-hours call

01

Message age

p50, p95 and the oldest message. Queue length alone, without arrival rate, can mislead.

02

Retry rate

Attempts by error type, dependency and contract version. Alert on changing trends, not a single retry.

03

DLQ ingress

New entries, reason, owner and time until a decision. An ownerless DLQ grows quietly.

04

Lock lost / timeout

Shows a mismatch between processing time and locks, or an overloaded consumer.

05

Duplicate prevented

A counter demonstrates that idempotency protects real outcomes, rather than merely appearing in documentation.

06

Outbox lag

The oldest unpublished record's age and dispatcher throughput form a basic integration SLO.

START

For beginners

  • Distinguish commands, events and document messages.
  • Learn when Complete/Delete is safe.
  • Add MessageId, correlationId and one duplicate test.
PRACTICE

For intermediate developers

  • Implement outbox and inbox with a unique index.
  • Match lock or visibility timeout to p95.
  • Build retries with backoff, jitter and a controlled DLQ.
SENIOR

For senior developers

  • Define SLOs, an error budget and queue ownership.
  • Test schema evolution and historical-data replay.
  • Calculate operating costs and design safe rollback.

Pre-production checklist

If even one answer is “I do not know”, the queue is not ready

  • Does every message have stable MessageId and correlationId values?
  • Are state and outbox persistence atomic?
  • Does a duplicate produce the same observable result?
  • Do large payloads use Blob Storage/S3 and claim-check?
  • Do retries distinguish transient from permanent errors?
  • Does the DLQ have an alert, owner and replay procedure?
  • Does lock/visibility timeout reflect actual p95?
  • Does the dashboard show message age, not just count?
  • Can we stop a consumer without losing accepted work?
  • Does a test reproduce timeout after the effect but before settlement?
FINALE · 10 QUESTIONS

Check whether you are designing a queue or merely delaying an outage

The quiz covers Azure Service Bus, Amazon SQS, outbox, inbox, claim-check, idempotency, retries and telemetry. After answering, verify your email with a one-time code. A dedicated report gives your score, level and next steps. Your address is not published on the blog.

Technical sources

This material uses vendor documentation current at publication: