Skip to content
Back to Blog
1 min read

Distributed Transactions in Azure SQL Managed Instance

I wrote “Distributed Transactions in Azure SQL Managed Instance” to share practical, production-minded guidance on this topic.

Understanding Distributed Transactions

Distributed transactions coordinate changes across multiple databases using the two-phase commit protocol. Managed Instance supports both cross-database and cross-instance transactions.

Cross-Database Transactions

-- Cross-database transaction within the same instance
USE master;
GO

-- Create test databases
CREATE DATABASE SalesDB;
CREATE DATABASE InventoryDB;
GO

-- Create tables
USE SalesDB;
CREATE TABLE dbo.Orders (
    OrderID INT IDENTITY(1,1) PRIMARY KEY,
    CustomerName NVARCHAR(100),
    ProductID INT,
    Quantity INT,
    OrderDate DATETIME2 DEFAULT GETUTCDATE()
);
GO

USE InventoryDB;
CREATE TABLE dbo.Products (
    ProductID INT PRIMARY KEY,
    ProductName NVARCHAR(100),
    StockQuantity INT
);

INSERT INTO dbo.Products VALUES (1, 'Widget', 100);
GO

-- Cross-database transaction
USE SalesDB;
GO

BEGIN TRANSACTION;

-- Insert order
INSERT INTO dbo.Orders (CustomerName, ProductID, Quantity)
VALUES ('Acme Corp', 1, 10);

-- Update inventory in another database
UPDATE InventoryDB.dbo.Products
SET StockQuantity = StockQuantity - 10
WHERE ProductID = 1;

-- Verify stock is not negative
IF EXISTS (SELECT 1 FROM InventoryDB.dbo.Products WHERE ProductID = 1 AND StockQuantity < 0)
BEGIN
    ROLLBACK TRANSACTION;
    THROW 50001, 'Insufficient stock', 1;
END

COMMIT TRANSACTION;

Cross-Instance Distributed Transactions

using System.Transactions;
using Microsoft.Data.SqlClient;

public class DistributedTransactionService
{
    private readonly string _instance1ConnectionString;
    private readonly string _instance2ConnectionString;

    public DistributedTransactionService(string conn1, string conn2)
    {
        _instance1ConnectionString = conn1;
        _instance2ConnectionString = conn2;
    }

    public async Task ExecuteDistributedTransactionAsync(Order order)
    {
        // TransactionScope automatically promotes to distributed transaction
        // when multiple connections are used
        var options = new TransactionOptions
        {
            IsolationLevel = IsolationLevel.ReadCommitted,
            Timeout = TimeSpan.FromMinutes(5)
        };

        using var scope = new TransactionScope(
            TransactionScopeOption.Required,
            options,
            TransactionScopeAsyncFlowOption.Enabled);

        try
        {
            // Operation on Instance 1 (Orders)
            using (var conn1 = new SqlConnection(_instance1ConnectionString))
            {
                await conn1.OpenAsync();

                var insertCmd = new SqlCommand(@"
                    INSERT INTO dbo.Orders (CustomerID, TotalAmount, OrderDate)
                    VALUES (@CustomerID, @TotalAmount, @OrderDate);
                    SELECT SCOPE_IDENTITY();", conn1);

                insertCmd.Parameters.AddWithValue("@CustomerID", order.CustomerID);
                insertCmd.Parameters.AddWithValue("@TotalAmount", order.TotalAmount);
                insertCmd.Parameters.AddWithValue("@OrderDate", DateTime.UtcNow);

                order.OrderID = Convert.ToInt32(await insertCmd.ExecuteScalarAsync());
            }

            // Operation on Instance 2 (Payments)
            using (var conn2 = new SqlConnection(_instance2ConnectionString))
            {
                await conn2.OpenAsync();

                var paymentCmd = new SqlCommand(@"
                    INSERT INTO dbo.PaymentRecords (OrderID, Amount, PaymentDate, Status)
                    VALUES (@OrderID, @Amount, @PaymentDate, 'Pending')", conn2);

                paymentCmd.Parameters.AddWithValue("@OrderID", order.OrderID);
                paymentCmd.Parameters.AddWithValue("@Amount", order.TotalAmount);
                paymentCmd.Parameters.AddWithValue("@PaymentDate", DateTime.UtcNow);

                await paymentCmd.ExecuteNonQueryAsync();
            }

            // Both operations succeeded - commit
            scope.Complete();
        }
        catch (Exception)
        {
            // Transaction will automatically rollback
            throw;
        }
    }
}

public class Order
{
    public int OrderID { get; set; }
    public int CustomerID { get; set; }
    public decimal TotalAmount { get; set; }
}

Linked Server Transactions

-- Create linked server to another Managed Instance
EXEC sp_addlinkedserver
    @server = N'MI_INVENTORY',
    @srvproduct = N'',
    @provider = N'MSOLEDBSQL',
    @datasrc = N'inventory-mi.public.eastus.database.windows.net',
    @catalog = N'InventoryDB';

-- Configure linked server security
EXEC sp_addlinkedsrvlogin
    @rmtsrvname = N'MI_INVENTORY',
    @useself = N'False',
    @locallogin = NULL,
    @rmtuser = N'linkeduser',
    @rmtpassword = N'StrongP@ssw0rd!';

-- Enable RPC and distributed transactions
EXEC sp_serveroption @server = N'MI_INVENTORY', @optname = 'rpc', @optvalue = 'true';
EXEC sp_serveroption @server = N'MI_INVENTORY', @optname = 'rpc out', @optvalue = 'true';

-- Distributed transaction with linked server
SET XACT_ABORT ON;

BEGIN DISTRIBUTED TRANSACTION;

-- Local operation
INSERT INTO dbo.Orders (CustomerName, ProductID, Quantity)
VALUES ('Beta Corp', 1, 5);

-- Remote operation via linked server
UPDATE MI_INVENTORY.InventoryDB.dbo.Products
SET StockQuantity = StockQuantity - 5
WHERE ProductID = 1;

COMMIT TRANSACTION;

Monitoring Distributed Transactions

-- View active distributed transactions
SELECT
    dt.transaction_id,
    dt.name AS transaction_name,
    dt.transaction_begin_time,
    dt.transaction_type,
    dt.transaction_state,
    DATEDIFF(second, dt.transaction_begin_time, GETDATE()) AS duration_seconds
FROM sys.dm_tran_active_transactions dt
WHERE dt.transaction_type = 2;  -- Distributed transaction

-- View transaction locks
SELECT
    t.transaction_id,
    l.request_session_id,
    l.resource_type,
    l.resource_database_id,
    DB_NAME(l.resource_database_id) AS database_name,
    l.request_mode,
    l.request_status
FROM sys.dm_tran_locks l
JOIN sys.dm_tran_active_transactions t ON l.request_owner_id = t.transaction_id
WHERE t.transaction_type = 2;

-- Check DTC status
SELECT * FROM sys.dm_tran_distributed_transaction_stats;

Handling Transaction Failures

public class ResilientTransactionService
{
    private readonly string _conn1;
    private readonly string _conn2;
    private readonly ILogger _logger;

    public async Task<TransactionResult> ExecuteWithRetryAsync(
        Func<SqlConnection, SqlConnection, Task> operation,
        int maxRetries = 3)
    {
        var attempt = 0;
        var delay = TimeSpan.FromSeconds(1);

        while (true)
        {
            try
            {
                var options = new TransactionOptions
                {
                    IsolationLevel = IsolationLevel.ReadCommitted,
                    Timeout = TimeSpan.FromMinutes(2)
                };

                using var scope = new TransactionScope(
                    TransactionScopeOption.Required,
                    options,
                    TransactionScopeAsyncFlowOption.Enabled);

                using var conn1 = new SqlConnection(_conn1);
                using var conn2 = new SqlConnection(_conn2);

                await conn1.OpenAsync();
                await conn2.OpenAsync();

                await operation(conn1, conn2);

                scope.Complete();

                return new TransactionResult { Success = true };
            }
            catch (TransactionAbortedException ex)
            {
                attempt++;
                _logger.LogWarning(ex,
                    "Transaction aborted, attempt {Attempt} of {MaxRetries}",
                    attempt, maxRetries);

                if (attempt >= maxRetries)
                {
                    return new TransactionResult
                    {
                        Success = false,
                        Error = ex.Message
                    };
                }

                await Task.Delay(delay);
                delay = TimeSpan.FromSeconds(delay.TotalSeconds * 2);
            }
            catch (SqlException ex) when (IsTransientError(ex))
            {
                attempt++;
                _logger.LogWarning(ex,
                    "Transient SQL error, attempt {Attempt} of {MaxRetries}",
                    attempt, maxRetries);

                if (attempt >= maxRetries)
                {
                    return new TransactionResult
                    {
                        Success = false,
                        Error = ex.Message
                    };
                }

                await Task.Delay(delay);
                delay = TimeSpan.FromSeconds(delay.TotalSeconds * 2);
            }
        }
    }

    private static bool IsTransientError(SqlException ex)
    {
        int[] transientErrors = { 1205, 3960, 41302, 41305, 41325, 41839 };
        return transientErrors.Contains(ex.Number);
    }
}

public class TransactionResult
{
    public bool Success { get; set; }
    public string Error { get; set; }
}

Best Practices

-- Always use XACT_ABORT for distributed transactions
SET XACT_ABORT ON;

-- Keep transactions short
BEGIN DISTRIBUTED TRANSACTION;
    -- Minimal operations only
COMMIT;

-- Use appropriate isolation levels
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- Handle deadlocks
BEGIN TRY
    BEGIN DISTRIBUTED TRANSACTION;
        -- Operations
    COMMIT;
END TRY
BEGIN CATCH
    IF XACT_STATE() <> 0
        ROLLBACK;

    IF ERROR_NUMBER() = 1205  -- Deadlock
    BEGIN
        WAITFOR DELAY '00:00:01';
        -- Retry logic
    END
    ELSE
        THROW;
END CATCH

Considerations

  1. Performance overhead - Distributed transactions have higher latency
  2. Network reliability - Depends on network connectivity between instances
  3. Timeout configuration - Set appropriate transaction timeouts
  4. Isolation level - Choose carefully for your consistency needs
  5. Error handling - Implement robust retry and compensation logic

Distributed transactions in Azure SQL Managed Instance enable enterprise-grade data consistency across complex architectures.\n\n## Takeaways\n\nAdd a concise, personal takeaway and recommended next steps here.\n

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.