Service Bus Transactions
Azure Service Bus Transactions allow multiple messaging operations to succeed or fail together as a single atomic unit. If any operation in the transaction fails, all other operations in the same transaction are rolled back automatically. No partial state is left behind. This is essential for workflows where two or more messaging actions must happen together or not at all.
What Is a Transaction in Service Bus?
A transaction groups a set of operations — such as completing a received message and sending a new message — into one atomic action. Either all operations commit or all roll back. This is the same concept as database transactions but applied to message operations.
WITHOUT Transaction: Step 1: Complete received message "Order-101" --> SUCCESS Step 2: Send new message "Invoice-101" --> FAILS (error) Result: Order-101 is GONE (completed) but Invoice-101 never sent. System is in inconsistent state.
WITH Transaction:
Begin Transaction
Step 1: Complete received message "Order-101" --> staged
Step 2: Send new message "Invoice-101" --> staged
Commit Transaction
If both succeed --> Both committed atomically.
If Step 2 fails --> Entire transaction rolls back.
Order-101 is NOT completed (it stays in the queue).
Invoice-101 is NOT sent.
System stays consistent.
Supported Transactional Operations
| Operation | Transactional? |
|---|---|
| Complete a message | Yes |
| Abandon a message | Yes |
| Dead-letter a message | Yes |
| Defer a message | Yes |
| Send a message to a queue | Yes |
| Send a message to a topic | Yes |
| Schedule a message | Yes |
| Cancel a scheduled message | Yes |
Transaction Scope — Via Entity Constraint
Service Bus transactions operate within a Via Entity. All transactional operations must go through one entity (the via entity). A transaction cannot span operations on completely unrelated, unconnected entities unless one is the via entity.
Valid transaction scope (single entity or via entity): - Receive from Queue A AND send to Queue B (via Queue A) - Complete from Queue A AND send to Queue A (same entity) - Receive from Queue A AND send to Topic X (via Queue A) Invalid (crossing namespaces): - Operations across two different namespaces in one transaction
Basic Transaction — .NET SDK
Pattern: Receive a Message and Send a New Message Atomically
using Azure.Messaging.ServiceBus;
await using var client = new ServiceBusClient(connectionString);
// Receiver for the order queue
await using var receiver = client.CreateReceiver("order-queue");
// Sender for the invoice queue (via entity = order-queue)
await using var sender = client.CreateSender("invoice-queue");
// Receive the order message
var orderMsg = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));
if (orderMsg != null)
{
Console.WriteLine($"Order received: {orderMsg.Body}");
// Create the invoice message to send
var invoiceMsg = new ServiceBusMessage($"Invoice for: {orderMsg.Body}")
{
MessageId = $"invoice-{orderMsg.MessageId}"
};
// Begin transaction
using var transaction = new ServiceBusTransactionContext();
try
{
// Complete the order message (inside transaction)
await receiver.CompleteMessageAsync(orderMsg, transaction);
// Send invoice message (inside transaction)
await sender.SendMessageAsync(invoiceMsg, transaction);
// Commit — both operations commit atomically
await transaction.CommitAsync();
Console.WriteLine("Transaction committed. Order completed and invoice sent.");
}
catch (Exception ex)
{
// Rollback — neither operation takes effect
await transaction.RollbackAsync();
Console.WriteLine($"Transaction rolled back: {ex.Message}");
}
}
Transaction via Entity — Correct Setup
When sending to a different queue inside a transaction, the sender must use the via entity path. The via entity is the queue the receiver is connected to. This tells Service Bus to route the send operation through the receive entity for transactional consistency.
// Correct setup: sender uses via-entity path
await using var receiver = client.CreateReceiver("order-queue");
// ViaQueueName ensures the send operation goes through order-queue transactionally
await using var sender = client.CreateSender("invoice-queue");
// Use TransactionContext tied to the receiver's entity
Transaction — Practical Example: Order to Payment
Scenario:
1. Read "OrderPlaced" message from order-queue
2. Send "ProcessPayment" message to payment-queue
3. Complete the "OrderPlaced" message
All three steps must succeed together. If payment queue fails, order stays in queue.
await using var orderReceiver = client.CreateReceiver("order-queue");
await using var paymentSender = client.CreateSender("payment-queue");
var orderMsg = await orderReceiver.ReceiveMessageAsync(TimeSpan.FromSeconds(10));
if (orderMsg != null)
{
var paymentMsg = new ServiceBusMessage($"Pay for order: {orderMsg.MessageId}")
{
MessageId = $"pay-{orderMsg.MessageId}"
};
using ServiceBusTransactionContext txn = new();
try
{
await paymentSender.SendMessageAsync(paymentMsg, txn);
await orderReceiver.CompleteMessageAsync(orderMsg, txn);
await txn.CommitAsync();
Console.WriteLine("Order completed and payment triggered atomically.");
}
catch
{
await txn.RollbackAsync();
Console.WriteLine("Transaction rolled back. Order message preserved.");
}
}
Transaction with Multiple Sends
// Send two messages atomically — both go or neither goes
using ServiceBusTransactionContext txn = new();
try
{
var msg1 = new ServiceBusMessage("Notification: Email");
var msg2 = new ServiceBusMessage("Notification: SMS");
await emailSender.SendMessageAsync(msg1, txn);
await smsSender.SendMessageAsync(msg2, txn);
await txn.CommitAsync();
Console.WriteLine("Both notifications sent atomically.");
}
catch
{
await txn.RollbackAsync();
Console.WriteLine("Transaction failed. No notifications sent.");
}
Transaction Rules and Limitations
| Rule | Detail |
|---|---|
| Via Entity Required | All sends to a different entity must use the via entity (receiver's entity) |
| Same Namespace Only | Transactions cannot span two different namespaces |
| Max Operations | Up to 100 operations per transaction |
| No Receives in Transaction | Receiving a message starts a lock but is not part of the transaction scope. Only settlement operations (Complete/Abandon) are transactional. |
| Tier Support | Transactions are available in Standard and Premium tiers. Not in Basic. |
Transactions vs Message Locks
Message Lock: Consumer reads message --> Lock starts (e.g., 60 seconds) Lock prevents other consumers from reading the same message during processing Lock expires --> message returns to queue Transaction: Wraps settlement operations + sends All operations commit or all rollback atomically Transaction rollback returns the received message to the queue automatically
Best Practices for Service Bus Transactions
- Keep transactions short — long-running transactions risk message lock expiry
- Always wrap transactions in try-catch-rollback to handle failures cleanly
- Use transactions when exactly-once delivery semantics are a strict requirement
- Avoid mixing non-Service Bus operations (database calls) inside Service Bus transactions — they have separate commit/rollback mechanisms
- Use the outbox pattern for cross-system atomicity (database + Service Bus)
Outbox Pattern — Cross-System Atomicity
Problem: Need to update database AND send message atomically
(Service Bus and SQL have different transaction managers)
Outbox Pattern:
Step 1: Write event to database outbox table (same DB transaction as business data)
Step 2: A background worker reads the outbox table
Step 3: Worker sends the event to Service Bus
Step 4: Worker marks the outbox record as "sent"
Result: Database and Service Bus are eventually consistent without a distributed transaction.
Summary
Azure Service Bus Transactions provide atomicity across multiple messaging operations. A group of operations — such as completing a received message and sending a new one — either all succeed or all roll back. Transactions require a via entity to ensure all operations flow through one entity for consistency. They work within a single namespace and support up to 100 operations per transaction. Transactions are available in Standard and Premium tiers and are essential for building reliable, consistent workflows where partial execution is not acceptable.
