To build SQL Server high availability on Azure VMs, place two or more SQL Server VMs in different subnets of the same virtual network and in different availability zones, join them to a Windows Server Failover Cluster with a cloud witness for quorum, and create an Always On availability group whose listener has one IP address in each subnet. With the multi-subnet design you need no Azure Load Balancer or distributed network name, and clients connect through the listener with MultiSubnetFailover=True so they reach the new primary quickly after a failover.
Who this is for and what you will have at the end
This guide is for DBAs and infrastructure engineers deploying SQL Server on Azure Virtual Machines who need automatic failover inside a region. It follows Microsoft's manual multi-subnet tutorial and adds the Azure-specific cluster tuning from the HADR best-practices article.
At the end you will have:
- Two SQL Server VMs in separate subnets and availability zones, joined to Active Directory.
- A two-node Windows Server Failover Cluster with a cloud witness and Azure-appropriate heartbeat thresholds.
- An availability group with synchronous commit, automatic failover, and a listener with an IP address in each subnet.
- A tested planned failover and a client connection string that survives it.
If you are moving an existing on-premises estate, the enterprise Azure migration playbook covers how availability groups fit into the wider migration plan.
Design decisions before you deploy
Subnets and listener connectivity
Azure doesn't allow broadcast traffic such as gratuitous ARP, so a listener IP can't simply move between VMs in one subnet as it does on-premises. That leaves three ways to route clients to the primary:
| Option | How it works | What you maintain |
|---|---|---|
| Multi-subnet (recommended) | Each replica sits in its own subnet; the listener has one IP per subnet and only the primary's IP is online | Secondary IPs on each VM NIC |
| VNN listener with Azure Load Balancer | Single subnet; a load balancer with a health probe sends traffic to the VM that answers the probe | Load balancer, probe port, Direct Server Return (floating IP) |
| DNN listener | Single subnet; a distributed network name resolves to all node IPs and the client finds the primary | A unique DNN port; requires SQL Server 2016 SP3, 2017 CU25 or 2019 CU8 or later on Windows Server 2016 or later |
Microsoft's guidance is to deploy to multiple subnets whenever possible, because it removes the dependency on both the load balancer and the DNN.
Zones or availability sets
A VM can be in an availability set or an availability zone, not both. Zones protect against a datacenter failure but add network latency between replicas, which can slow synchronous commit. Availability sets, optionally in a proximity placement group, keep replicas close. Test under load before choosing.
Quorum
| Witness | Supported OS | When to use |
|---|---|---|
| Cloud witness | Windows Server 2016 and later | Default choice for availability groups on Azure, including multi-zone and multi-region designs |
| Disk witness | All | Clusters that use Azure Shared Disks or other shared storage |
| File share witness | All | When neither of the others is available |
A two-node cluster technically runs without a witness, but Microsoft requires a quorum resource for production support, and cluster validation doesn't pass without one.
IP address plan
The example uses a virtual network with a domain controller subnet and one subnet per SQL Server VM. On Windows Server 2019 and later the cluster uses a distributed network name for its own name, so each VM needs only one secondary IP, for the listener. On Windows Server 2016 and earlier, or if you deliberately create the cluster with a virtual network name, each VM also needs a secondary IP for the cluster.
| VM | Subnet | Primary IP | Cluster IP (WS2016 and earlier) | Listener IP |
|---|---|---|---|---|
| SQL-VM-1 | SQL-subnet-1, 10.38.1.0/24 | Assigned at creation | 10.38.1.10 | 10.38.1.11 |
| SQL-VM-2 | SQL-subnet-2, 10.38.2.0/24 | Assigned at creation | 10.38.2.10 | 10.38.2.11 |
Prerequisites
- An Azure subscription and a resource group in one region.
- A virtual network with three subnets: one for domain controllers and one for each SQL Server VM.
- Active Directory Domain Services reachable from the virtual network, with the virtual network's DNS servers set to the domain controllers.
- Two domain accounts: an installation account (for example
CORP\Install) and a SQL Server service account (for exampleCORP\SQLSvc). The installation account needs Read all properties and Create Computer Objects on the domain or OU, because creating the cluster and listener creates computer objects. - A Standard general-purpose v2 storage account with locally redundant storage for the cloud witness.
- SQL Server Enterprise or Standard on each VM; the manual multi-subnet method supports both. A basic availability group allows only one secondary replica and no read access to it.
Step 1: Create the SQL Server VMs
Create both VMs from a SQL Server image in the same region and virtual network, placing SQL-VM-1 in SQL-subnet-1 and zone 1, and SQL-VM-2 in SQL-subnet-2 and zone 2 (or both in one availability set where zones aren't available). Use managed disks and private IP addresses only in production. Use one NIC per VM: Azure networking is physically redundant, extra NICs share the same bandwidth limit, and the validation warning about a single network can be ignored on Azure.
Add secondary IP addresses
For each VM, open the VM in the Azure portal, select Networking, select the network interface, then IP configurations > + Add. Add a configuration named availability-group-listener with Allocation set to Static and an unused address in that VM's subnet. On Windows Server 2016 or earlier, also add windows-cluster-ip the same way. Leave Public IP address disassociated.
Join the domain and set accounts
- Join both VMs to the domain and restart them.
- Add the installation account to the local Administrators group on each VM, and to the
sysadminfixed server role in each SQL Server instance. - Set the SQL Server service on both VMs to run as the domain service account in SQL Server Configuration Manager, which restarts the service.
- Grant the local system account the permissions the cluster needs to manage the availability group. In current SQL Server versions
NT AUTHORITY\SYSTEMhas no SQL Server permissions by default:
USE [master];
GO
CREATE LOGIN [NT AUTHORITY\SYSTEM] FROM WINDOWS WITH DEFAULT_DATABASE = [master];
GO
GRANT ALTER ANY AVAILABILITY GROUP TO [NT AUTHORITY\SYSTEM];
GRANT CONNECT SQL TO [NT AUTHORITY\SYSTEM];
GRANT VIEW SERVER STATE TO [NT AUTHORITY\SYSTEM];
GOOpen the firewall
On both VMs, allow inbound TCP 1433 for the default instance and TCP 5022 (or whichever port you choose) for the database mirroring endpoint. If you use a network security group between the subnets, allow the same ports there, along with the Windows cluster service ports.
Step 2: Create the Windows Server Failover Cluster
Install the Failover Clustering feature on both VMs from Server Manager > Add roles and features > Features.
Sign in to SQL-VM-1 as the installation account and validate the configuration. Skip the storage tests, because availability groups don't use shared storage:
Test-Cluster -Node "SQL-VM-1","SQL-VM-2" -Include "Inventory","Network","System Configuration"Create the cluster without storage. On Windows Server 2019 or later:
New-Cluster -Name SQLAGCluster1 -Node SQL-VM-1,SQL-VM-2 -NoStorageOn Windows Server 2016 or earlier, pass the dedicated cluster IP from each subnet:
New-Cluster -Name SQLAGCluster1 -Node SQL-VM-1,SQL-VM-2 -StaticAddress 10.38.1.10,10.38.2.10 -NoStorageIf you use the Failover Cluster Manager wizard instead, clear Add all eligible storage to the cluster on the confirmation page. Otherwise Windows detaches the data disks, and they don't reappear in Disk Management until you remove them from the cluster.
On Windows Server 2016 and earlier, the cluster's IP resources may come up as Failed if they took the node's own address. In Cluster Core Resources, open each failed IP Address resource, set the static cluster IP for its subnet, and bring the cluster name online. The name resource has an OR dependency on the two IPs, so one is online at a time.
Step 3: Configure the cloud witness and quorum votes
Copy an access key from the storage account (Security + networking > Access keys), then on SQL-VM-1 in an elevated PowerShell session:
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Set-ClusterQuorum -CloudWitness -AccountName "<storage-account-name>" -AccessKey "<storage-account-access-key>"Give votes only to nodes that can become primary through automatic failover, keep an odd total of at least three (two nodes plus the witness here), and remove votes from disaster recovery nodes. Check the weights with:
Get-ClusterNode | Format-Table -Property NodeName, State, NodeWeightStep 4: Relax cluster heartbeat thresholds for Azure
The default thresholds are designed for tuned on-premises networks. The heartbeat runs over UDP 3343, and transient latency or Azure platform maintenance can otherwise trigger needless failovers. For Windows Server 2012 and later Microsoft recommends a delay of 1 second and a threshold of 40 heartbeats for both same-subnet and cross-subnet traffic:
(Get-Cluster).SameSubnetThreshold = 40
(Get-Cluster).CrossSubnetThreshold = 40
Get-Cluster | Format-List *subnet*The change is immediate. Keep same-subnet values no greater than cross-subnet values. For comparison, Windows Server 2019 defaults to 20 heartbeats and Windows Server 2016 to 10.
Step 5: Enable Always On and prepare the database
Enable the feature on both instances. In SQL Server Configuration Manager, open the SQL Server (MSSQLSERVER) service properties, select the Always On High Availability tab, check Enable Always On availability groups, and restart the service. With the SqlServer PowerShell module the equivalent is:
Enable-SqlAlwaysOn -ServerInstance "SQL-VM-1" -Force
Enable-SqlAlwaysOn -ServerInstance "SQL-VM-2" -Force-Force restarts the database engine without prompting, so run it in a maintenance window.
A database must be in the full recovery model and have at least one full backup before it can join an availability group:
ALTER DATABASE [SalesDB] SET RECOVERY FULL;
BACKUP DATABASE [SalesDB] TO DISK = N'\\SQL-VM-1\Backup\SalesDB.bak';Create the Backup share on SQL-VM-1 with full control for the SQL Server service account if you plan to use the wizard's full database and log backup initialization.
Step 6: Create the availability group and listener
In SQL Server Management Studio on SQL-VM-1, right-click Always On High Availability and select New Availability Group Wizard.
- Name the group, for example
AG1, and select the database. - On Specify Replicas, add SQL-VM-2, check Automatic Failover for both replicas and set Availability Mode to Synchronous commit.
- On the Endpoints tab, confirm the endpoint port matches the one you opened (5022 in this guide).
- On the Listener tab, create a listener named
AG1-Listeneron port 1433 with Static IP network mode. Select Add twice: once for 10.38.1.0/24 with 10.38.1.11, and once for 10.38.2.0/24 with 10.38.2.11. - On Select Initial Data Synchronization, choose Full database and log backup with the share path. For large databases, restore a backup on the secondary
WITH NORECOVERYbeforehand and choose Join only. - Review validation, finish, and confirm the results page shows the group and listener created.
Perform all failovers from SQL Server Management Studio or T-SQL, not from Failover Cluster Manager.
Step 7: Configure clients
Clients should connect to the listener name with MultiSubnetFailover=True, which makes the driver try all listener IPs in parallel:
Server=tcp:AG1-Listener,1433;Database=SalesDB;Integrated Security=true;MultiSubnetFailover=TrueAvailability groups created with SSMS, T-SQL or PowerShell register all listener IPs in DNS (RegisterAllProvidersIP = 1). Check it with:
Get-ClusterResource | Where-Object {$_.ResourceType.Name -eq "Network Name"} | Get-ClusterParameter | Where-Object {$_.Name -eq "RegisterAllProvidersIP"}If a legacy client can't use MultiSubnetFailover, set RegisterAllProvidersIP to 0 and HostRecordTTL to 300 on the listener resource so the client re-resolves faster after failover, accepting more DNS queries:
Get-ClusterResource yourListenerName | Set-ClusterParameter RegisterAllProvidersIP 0
Get-ClusterResource yourListenerName | Set-ClusterParameter HostRecordTTL 300Replace yourListenerName with the listener's network name resource, shown on the Resources tab of the availability group role in Failover Cluster Manager.
Verification
Check synchronization and failover readiness on the primary:
SELECT ag.name AS ag_name,
ar.replica_server_name,
drs.synchronization_state_desc,
drcs.database_name,
drcs.is_failover_ready
FROM sys.dm_hadr_database_replica_states AS drs
JOIN sys.availability_groups AS ag ON drs.group_id = ag.group_id
JOIN sys.availability_replicas AS ar ON drs.replica_id = ar.replica_id
JOIN sys.dm_hadr_database_replica_cluster_states AS drcs
ON drs.replica_id = drcs.replica_id AND drs.group_database_id = drcs.group_database_id;Every database should show SYNCHRONIZED and is_failover_ready = 1 on the secondary. The Show Dashboard view in SSMS gives the same information.
Test the listener from a VM that isn't a replica: connect SSMS to AG1-Listener with MultiSubnetFailover=True under Options > Additional Connection Parameters, and run SELECT @@SERVERNAME;.
Then perform a planned manual failover. It is supported only when both replicas are in synchronous commit and synchronized, and it must be run on the target secondary:
-- Run on SQL-VM-2
ALTER AVAILABILITY GROUP AG1 FAILOVER;Reconnect through the listener, confirm @@SERVERNAME returns SQL-VM-2, then fail back from SQL-VM-1. Open transactions are rolled back during failover, so applications need retry logic.
Troubleshooting
Error 1135, "Cluster node was removed from the active failover cluster membership". Heartbeat thresholds are too aggressive for Azure. Apply the values in step 4, then check the VM's Activity Log and Resource Health for platform events at the same time.
Error 19407, "The lease between availability group and the Windows Server Failover Cluster has expired", or error 19419. Monitoring is too aggressive or the VM is starved of CPU or I/O. First look for VM and disk throttling in the VM Metrics (for example VM Cached Bandwidth Consumed Percentage and VM Uncached Bandwidth Consumed Percentage). If failovers continue after fixing performance, relax the settings below.
| Setting | Default | Relaxed starting point |
|---|---|---|
| Lease timeout | 20 seconds | 40 seconds, never more than 2 x SameSubnetThreshold x SameSubnetDelay (80 seconds with the step 4 values) |
| Health check timeout | 30 seconds | 60 seconds |
| Failure condition level | 3 | 2 |
| Session timeout | 10 seconds | 20 seconds |
| Max failures in specified period | 2 | 6 |
ALTER AVAILABILITY GROUP AG1 SET (HEALTH_CHECK_TIMEOUT = 60000);
ALTER AVAILABILITY GROUP AG1 SET (FAILURE_CONDITION_LEVEL = 2);
ALTER AVAILABILITY GROUP AG1 MODIFY REPLICA ON 'SQL-VM-1' WITH (SESSION_TIMEOUT = 20);Change lease timeout and max failures in Failover Cluster Manager. Don't lower any value below its default; relaxed monitoring can mask an underlying problem.
Errors 35201 or 35206, connection timeout to an availability replica. Check the endpoint port in Windows Firewall and network security groups and the endpoint URL; if the network is healthy, raise the session timeout.
"Not failing over group ... failoverCount 3, failoverThresholdSetting ..." in the cluster log. The group exhausted its maximum failures in the specified period and stayed failed. Raise Max failures in specified period on the role's Failover tab and fix the cause of the repeated failures.
Event 1206, cluster network name can't be brought online. The computer object couldn't be created or updated in Active Directory. Confirm the installation account, or the cluster name object, has permission to create computer objects in the target OU.
No cluster IP on Windows Server 2019 with an older SQL Server. Windows Server 2019 creates a distributed network name for the cluster. Microsoft's troubleshooting notes that this is only supported with SQL Server 2019; for earlier versions, recreate the cluster with a network name (for example New-Cluster with -ManagementPointNetworkType Singleton and static addresses).
Single-subnet VNN only: Event 1069 with status 10048 on the probe port. Another process took the load balancer probe port. Exclude it from the dynamic range, for example netsh int ipv4 add excludedportrange tcp startport=59999 numberofports=1 store=persistent.
Checklist
- Replicas in separate subnets and separate zones (or one availability set), one NIC each, private IPs only.
- Listener secondary IP on each VM NIC; cluster IP too on Windows Server 2016 and earlier.
- Installation account can create computer objects;
NT AUTHORITY\SYSTEMhas the three availability group permissions. - Cluster validated without storage tests and created with
-NoStorage. - Cloud witness on a Standard GPv2 LRS storage account; odd number of votes; no votes in DR regions.
SameSubnetThresholdandCrossSubnetThresholdset to 40.- Databases in full recovery with a full backup; synchronous commit and automatic failover between the two replicas.
- Listener with one IP per subnet; clients use
MultiSubnetFailover=True. - Planned failover and failback tested, and alerting on synchronization state and lease errors in place.
References
- Overview of Always On availability groups on Azure VMs
- Tutorial: Prerequisites for an availability group in multiple subnets
- Tutorial: Configure an availability group in multiple subnets
- HADR configuration best practices for SQL Server on Azure VMs
- New-Cluster
- Enable-SqlAlwaysOn
- Configure a Windows failover cluster with Storage Spaces Direct (Test-Cluster usage)
- Perform a planned manual failover of an availability group