Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Important
Distributed transactions in Azure Cosmos DB are currently in public preview. This preview is provided without a service-level agreement (SLA). Behavior, limits, and supported scenarios could change before general availability.
This article shows you how to enable distributed transactions on an Azure Cosmos DB for NoSQL account and use them from the .NET SDK to commit atomic read and write operations that span multiple logical partitions, containers, and databases within the same account and region.
The examples in this article use a single scenario - a banking database with two containers, accounts (partitioned by account ID) and ledger (partitioned by posting month) - so the same items appear across the read and write examples.
Prerequisites
Before you begin, make sure you have:
- An active Azure subscription. If you don't have one, create a free account.
- An Azure Cosmos DB for NoSQL account. The account must:
- Use the NoSQL (Core SQL) API. MongoDB, Cassandra, Table, and Gremlin APIs aren't supported in preview.
- Be a provisioned throughput (manual or autoscale) account. Serverless accounts aren't supported in preview.
- Run on a public Azure cloud region. Sovereign, air-gapped, and government clouds aren't supported in preview.
- Be a single-write-region account. Multi-region write accounts aren't supported in preview.
- Not be configured with any of the following features:
- Customer-managed keys (CMK)
- Per-partition automatic failover (PPAF)
- Continuous backup
- Long-term retention
- Partition merge
- Hierarchical partition keys (HPK)
- Fabric native databases
- Minimum required version of the Azure Cosmos DB .NET v3 SDK (
Microsoft.Azure.Cosmos) from NuGet is v3.64.0-preview.0.
Request enrollment for your account
Distributed transactions are a public preview feature. Self-service enrollment through the Azure portal, Azure CLI, or PowerShell isn't currently available.
To request enrollment, submit the distributed transactions onboarding form. Requests are typically fulfilled within one to two business days. You receive a confirmation after your account is ready.
Install the required .NET SDK
Add the latest preview version (v3.64.0-preview.0) of the Azure Cosmos DB .NET SDK to your project.
dotnet add package Microsoft.Azure.Cosmos --version 3.64.0-preview.0
Initialize the client
Add these imports to the top of your C# file.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using Microsoft.Azure.Cosmos;
Create the client with transaction retry options. The values shown are the defaults.
CosmosClientOptions options = new CosmosClientOptions
{
MaxRetryAttemptsOnAbortedTransactions = 10,
MaxRetryWaitTimeOnAbortedTransactions = TimeSpan.FromSeconds(30)
};
The SDK automatically retries transactions that the service reports as retriable, honoring server retry-after hints. By default, it retries up to 10 times or until 30 seconds of cumulative retry delay, whichever comes first; this isn't an execution timeout. Set MaxRetryAttemptsOnAbortedTransactions to 0 to disable these retries. Throttled requests use the separate MaxRetryAttemptsOnRateLimitedRequests and MaxRetryWaitTimeOnRateLimitedRequests settings.
Initialize a CosmosClient against your enrolled account by using either Microsoft Entra ID or an account key.
Use Microsoft Entra ID for production. To set up your account with the required role assignments and credentials, see Use role-based access control to connect to Azure Cosmos DB for NoSQL.
using Microsoft.Azure.Cosmos;
using Azure.Identity;
string endpoint = "https://<your-account>.documents.azure.com:443/";
using CosmosClient client = new CosmosClient(
endpoint, new Azure.Identity.DefaultAzureCredential(), options);
The identity you use must have data-plane write permissions (for example, the Cosmos DB Built-in Data Contributor role) on every container that participates in a transaction. Role checks occur per individual operation inside the transaction batch.
You can get the container reference by using the GetContainer method on the CosmosClient instance. For example:
Container accounts = client.GetContainer("banking", "accounts");
Container ledger = client.GetContainer("banking", "ledger");
Commit a multi-partition write transaction
The .NET v3 SDK adds the CreateDistributedWriteTransaction() API to CosmosClient. Chain one operation per item, then call ExecuteTransactionAsync to submit the entire batch as a single atomic unit.
The following example atomically transfers 100 units from account-A to account-B and records the corresponding entry in the ledger container. The two accounts live in different logical partitions of the accounts container, and the ledger entry lives in a separate container.
The examples assume both containers use the partition key path /pk and that the account items already exist. Each write example is independent; run only one because each creates the ledger entry txn-1001.
These fixed balances assume both accounts start at 1000. For concurrent transfers, use a conditional patch.
var updatedAccountA = new { id = "account-A", pk = "account-A", balance = 900.00 };
var updatedAccountB = new { id = "account-B", pk = "account-B", balance = 1100.00 };
var ledgerEntry = new
{
id = "txn-1001", pk = "2026-09",
from = "account-A", to = "account-B", amount = 100.00
};
using DistributedTransactionResponse writeResponse = await client
.CreateDistributedWriteTransaction()
.ReplaceItem(accounts, new PartitionKey("account-A"), "account-A", updatedAccountA)
.ReplaceItem(accounts, new PartitionKey("account-B"), "account-B", updatedAccountB)
.CreateItem(ledger, new PartitionKey("2026-09"), ledgerEntry.id, ledgerEntry)
.ExecuteTransactionAsync(CancellationToken.None);
if (!writeResponse.IsSuccessStatusCode ||
writeResponse.Any(operation => !operation.IsSuccessStatusCode))
{
throw new InvalidOperationException(
$"Transaction failed: {writeResponse.StatusCode}; {writeResponse.ErrorMessage}; " +
$"ActivityId={writeResponse.ActivityId}; Diagnostics={writeResponse.Diagnostics}");
}
Console.WriteLine(writeResponse.ResponseMode == DistributedTransactionResponseMode.Standard
? "Transaction committed."
: "FastResponse received; do not infer a terminal commit outcome from this response.");
Commit all three items together or none of them. There's no partial state.
Important
Writes remain all-or-nothing. In FastResponse mode, the call can return before the final commit or abort. ResponseMode reports the mode the service used (Standard if not reported); it isn't a client setting.
Read and write transaction instances are single-use, even after a failure or cancellation; create a new instance to run again. A canceled call doesn't prove the transaction rolled back, so don't blindly replay it.
Mix operation types in a single transaction
You can mix CreateItem, UpsertItem, ReplaceItem, PatchItem, and DeleteItem within the same transaction.
using DistributedTransactionResponse patchResponse = await client
.CreateDistributedWriteTransaction()
.PatchItem(accounts, new PartitionKey("account-A"), "account-A",
new[] { PatchOperation.Increment("/balance", -100L) },
new DistributedTransactionPatchItemRequestOptions
{
FilterPredicate = "from c where c.balance >= 100"
})
.PatchItem(accounts, new PartitionKey("account-B"), "account-B",
new[] { PatchOperation.Increment("/balance", 100L) })
.CreateItem(ledger, new PartitionKey("2026-09"), ledgerEntry.id, ledgerEntry)
.ExecuteTransactionAsync(CancellationToken.None);
Console.WriteLine(
$"Status={patchResponse.StatusCode}; Mode={patchResponse.ResponseMode}; " +
$"Error={patchResponse.ErrorMessage}; Diagnostics={patchResponse.Diagnostics}");
Commit a multi-partition read transaction
Use CreateDistributedReadTransaction() when you need a point-in-time consistent snapshot of items that live in different logical partitions, containers, or databases. Unlike issuing several independent ReadItemAsync calls, a distributed read transaction returns all items as they existed at a single committed instant, so the reader never observes a partially applied write transaction.
Chain one ReadItem call per item, and then call CommitTransactionAsync to fetch the snapshot:
using DistributedTransactionResponse readResponse = await client
.CreateDistributedReadTransaction()
.ReadItem(accounts, new PartitionKey("account-A"), "account-A")
.ReadItem(accounts, new PartitionKey("account-B"), "account-B")
.ExecuteTransactionAsync(CancellationToken.None);
if (!readResponse.IsSuccessStatusCode &&
readResponse.StatusCode != HttpStatusCode.NotModified)
{
throw new InvalidOperationException(
$"Read failed: {readResponse.StatusCode}; {readResponse.ErrorMessage}; " +
$"Diagnostics={readResponse.Diagnostics}");
}
for (int i = 0; i < readResponse.Count; i++)
{
if (readResponse[i].StatusCode == HttpStatusCode.NotModified)
{
Console.WriteLine($"Operation {i}: unchanged.");
continue;
}
if (!readResponse[i].IsSuccessStatusCode)
{
throw new InvalidOperationException(
$"Read operation {i} failed: {readResponse[i].StatusCode}; " +
$"Diagnostics={readResponse.Diagnostics}");
}
Dictionary<string, object> account =
readResponse.GetOperationResultAtIndex<Dictionary<string, object>>(i).Resource;
Console.WriteLine($"{account["id"]}: {account["balance"]}");
}
Results follow request order. With IfNoneMatchEtag, unchanged items return 304; if all are unchanged, the transaction returns 304 with results preserved. IsSuccessStatusCode is false for 304, so check each operation. Dispose the response when done.
When to use a distributed read transaction
Distributed read transactions are most useful when correctness depends on the mutual consistency of items spread across partitions. Common scenarios include:
- Cross-account balance reconciliation. Read every account balance involved in a multileg funds transfer to confirm that debits and credits sum to zero, without the risk of reading one leg before and the other after a concurrent transfer commits.
- Inventory and order validation. Read a stock item and the corresponding pending-orders record together before deciding whether to accept a new order, so the available quantity and reserved quantity always reflect the same instant.
- Audit and compliance snapshots. Capture a coherent view of related records (for example, an order, its line items, and the customer profile that live in different containers) for reporting, exports, or regulatory evidence.
- Cache or read-model rebuilds. Hydrate a denormalized view or materialized projection from several source containers without seeing torn writes from in-flight distributed write transactions.
For single-item reads, or for unrelated items where mutual consistency isn't required, continue to use ReadItemAsync - it has lower latency and consumes fewer request units.
Multi-region considerations
In a multiregion account, distributed transactions are atomic within the write region only. Preview doesn't support multiregion write accounts - the account must have a single write region.
- The SDK routes all transactional reads and writes to the account's write region.
- Committed data replicates to secondary regions asynchronously and per-partition. Readers in secondary regions might temporarily observe partial updates until replication catches up.
For applications that require global read-after-write of transactional data, either:
- Route reads to the write region, or
- Use strong consistency at the account level, or
- Use the session token returned by the commit response.
Session tokens are per operation. The same client applies them automatically to later non-transactional session reads, but not to transaction operations; pass each token unchanged to later transaction operations on the same partition. Tokens provide partition-level read-your-writes, not a global atomic snapshot.
string accountAToken = writeResponse[0].SessionToken;
string accountBToken = writeResponse[1].SessionToken;
if (string.IsNullOrEmpty(accountAToken) || string.IsNullOrEmpty(accountBToken))
{
throw new InvalidOperationException(
"The response did not provide both tokens required for explicit session propagation.");
}
using DistributedTransactionResponse sessionReadResponse = await client
.CreateDistributedReadTransaction()
.ReadItem(accounts, new PartitionKey("account-A"), "account-A",
new DistributedTransactionRequestOptions { SessionToken = accountAToken })
.ReadItem(accounts, new PartitionKey("account-B"), "account-B",
new DistributedTransactionRequestOptions { SessionToken = accountBToken })
.ExecuteTransactionAsync(CancellationToken.None);
Console.WriteLine(
$"Status={sessionReadResponse.StatusCode}; Error={sessionReadResponse.ErrorMessage}; " +
$"Diagnostics={sessionReadResponse.Diagnostics}");
Limits in public preview
| Limit | Value |
|---|---|
| Maximum operations per transaction | 100 |
| Maximum payload size per transaction | 2 MB |
These limits might change before general availability.
Supported APIs and SDKs
Currently, only the NoSQL (Core SQL) API supports distributed transactions.
The .NET v3 SDK supports distributed transactions. Support for other SDKs is coming soon.