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.
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.
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.
Save intent and give it a stable identifier.
Do not make users wait for an external system.
Retries are a normal design condition.
Metrics, logs and receipts must demonstrate one effect.
A map to keep
From request to effect: one contract, two transports
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 →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.
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 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
| Need | Azure Service Bus | Amazon SQS | Design decision |
|---|---|---|---|
| Basic delivery | PeekLock provides at-least-once semantics | Standard Queue provides at-least-once delivery | Consumers must tolerate repetition |
| Ordering within a group | Sessions with SessionId | FIFO with MessageGroupId | Group by aggregate, not the whole system |
| Publication duplicates | Duplicate detection using MessageId | FIFO deduplication using a deduplication ID | An additional layer, not an inbox replacement |
| Processing time | Lock duration and renewal | Visibility timeout and extension | Set above p95 and monitor overruns |
| Poison messages | Built-in dead-letter queue | Redrive policy to a DLQ | Alert, owner, retry policy and replay procedure |
| Large data | Blob Storage + a key in the message | S3 + a key in the message | Claim-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.
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.
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);
}
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.
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.
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.
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);
}
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.
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.
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
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
Lab ready. Start the controlled sequence.
Production runbook
What to monitor before the after-hours call
Message age
p50, p95 and the oldest message. Queue length alone, without arrival rate, can mislead.
Retry rate
Attempts by error type, dependency and contract version. Alert on changing trends, not a single retry.
DLQ ingress
New entries, reason, owner and time until a decision. An ownerless DLQ grows quietly.
Lock lost / timeout
Shows a mismatch between processing time and locks, or an overloaded consumer.
Duplicate prevented
A counter demonstrates that idempotency protects real outcomes, rather than merely appearing in documentation.
Outbox lag
The oldest unpublished record's age and dispatcher throughput form a basic integration SLO.
For beginners
- Distinguish commands, events and document messages.
- Learn when Complete/Delete is safe.
- Add MessageId, correlationId and one duplicate test.
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.
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?
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:
- Microsoft Learn: What's new in C# 14
- Microsoft Learn: What's new in .NET 10
- Azure Service Bus: message transfers, locks and duplicates
- Azure Service Bus: duplicate detection
- Azure Service Bus: message sessions
- AWS SDK for .NET V4: Amazon SQS APIs
- AWS Message Processing Framework for .NET
- Amazon SQS: at-least-once delivery
- Amazon SQS: visibility timeout
- Amazon SQS: dead-letter queues