Topic page · SQL Server high availability

The matrix is short. Operations are long.

If you have read part 1, you know what a failover cluster instance (FCI) protects and what an availability group (AG) protects. The next question is more concrete: what can each option do in my version and edition, what does it really protect me from, and what does it cost me day after day? The answer has two parts. The feature matrix fits on one screen. The operational consequences fill the rest of this article: commit latency, failover time, stalled secondaries, logins and jobs, patching. And because Microsoft gives no numbers for duration and latency, we measured them in the lab.

Updated Nov 8, 2026 · approx. 23 min read

Part 2 · Features & operationsTopic Matrix · RPO/RTO · latencyLab SQL Server 2022, query 2017–2025
FCIBasic AGFull AGWhat can it do?What does it cost?1 instance1 copy of the dataStd 2 · Ent 16 nodes1 database2 replicasStandard onlyup to 9 replicasreadable copiesEnterprise only

In short

  1. Standard gives you an FCI with two nodes and the basic AG with two replicas and one database. Anything that means reading or backing up on a copy is Enterprise. SQL Server 2025 doesn't change that.
  2. RPO and RTO are requirements, not product features. With an AG you can estimate them from the log send queue, the redo backlog and last_commit_time. With an FCI there is no data loss, but also no metric for restart time other than the recovery itself.
  3. Synchronous commit costs every transaction the latency of the link to the secondary. In the lab, on an internal network, median commit latency rose from 8 to 16 milliseconds; in practice, the distance between the replicas sets the surcharge.
  4. A role change in an AG takes less than a second, an instance restart about ten, and that restart is exactly what an FCI failover is. In both cases the recovery of open transactions comes on top. Accelerated database recovery (ADR) keeps their undo small, and an overly strict REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT can turn a one-second failover into a minute without write access.
  5. A stalled secondary is the primary's problem: as long as it is suspended, the primary's log can't be truncated.
  6. The daily price of an AG is the instance objects. Logins, jobs, linked servers and configuration belong to the instance and have to be maintained on every replica. The FCI doesn't have this problem, because there is only one instance.

The matrix: FCI, basic AG and full AG for SQL Server 2017 to 2025

The table summarizes Microsoft's edition and feature pages for the four versions. "Std" is Standard, "Ent" is Enterprise. Where nothing has changed since 2017, there is only one entry.

FeatureFCIBasic AGFull AG
EditionStd (2 nodes), Ent (up to 16 nodes)Std onlyEnt only
Protection levelinstanceone database per groupa group of databases
Data copiesone, on shared storagetwoone per replica, up to nine replicas
Synchronous / asynchronousnot applicableboth possibleboth; several synchronous replicas (number depends on the version, see below)
Automatic failoveryes, through the WSFC policyyes, with synchronous commityes, with synchronous commit and a synchronized target; 2025: fast failover with RestartThreshold 0
Readable secondaryno, passive nodes are stoppednoyes; 2025 with persisted statistics
Backups on the secondarynot applicableno, no integrity checks eitherlog and copy-only full backups; 2025 also full and differential backups
Automatic seedingnot applicableyesyes (since 2016; the T-SQL default is MANUAL)
Distributed AGcan host a replica of a distributed AG, manual failover onlynoyes, Ent; 2025 also with contained AGs
Contained AG (logins and jobs inside the group)not applicablenofrom 2022, Ent
Without a cluster (cluster type NONE)nofrom 2019 (2017: Ent only, as read-scale)from 2017, read scale and migration only, no high availability
Listener / namevirtual network name, alwayslistener optionallistener optional; without it, no redirection and no read-only routing
Query Store on the secondarynot applicableno2022 only with a trace flag, 2025 as a preview, not on by default
What goes along in a failoverthe whole instanceonly the databaseonly the databases of the group (contained AG: plus logins and jobs)
A third option for comparisonLog shipping: Std and Ent, manual role change, delayed restore as protection against user error

Two notes on numbers that people often ask about. Synchronous replicas: Microsoft states the maximum differently on different pages, from three to six. All that is certain is this: 2017 allows fewer than 2019 and later, and more than two. We therefore give no number until we have verified it ourselves. FCI nodes: the edition pages for all four versions say 16 for Enterprise; that is the SQL Server limit, not the limit of the Windows cluster.

RPO and RTO: requirements, not product features

RPO (recovery point objective) is the amount of data, measured in time, that you can afford to lose in a disaster. RTO (recovery time objective) is the time until the application is running again. Both are your requirements for the architecture, not properties of an FCI or an AG. The technologies only give you levers to meet the requirements and metrics to verify that you do.

With an AG, you can estimate both, and Microsoft publishes the formulas:

  • Recovery time = detection + redo + overhead. The overhead is the switch in the cluster and bringing the databases online, usually short and constant according to Microsoft. Detection depends on the cluster settings (health check timeout 30 seconds, lease timeout 20 seconds, session timeout 10 seconds are the defaults). Redo is the part you can influence: redo_queue_size divided by redo_rate, both in sys.dm_hadr_database_replica_states, in KB and KB/s. A secondary that can't keep up with replaying the log is usable in a failover only after it has caught up. The database with the largest backlog is what counts.
  • Data loss = log send queue divided by log generation rate. log_send_queue_size is the log the primary has not yet sent to the secondary; with synchronous commit and a synchronized secondary it is zero, with asynchronous commit it is the actual risk metric. As a simplification, the difference in last_commit_time between the primary and secondary rows is enough; Microsoft recommends checking this difference regularly against your target RPO and alerting on it.

With an FCI, there is no second copy and therefore no data loss, but also no queue you could watch. Restart time is service start plus the recovery of each database: analysis, redo, undo. What drives it is the dirty pages at the time of the failover (indirect checkpoints limit them) and the longest open transaction. How large that effect is shows in the section on failover duration.

Query 1 at the end of this article reads exactly these values: role, mode, log send queue, redo backlog, last_commit_time and the log wait reason, for a single instance, an FCI and an AG.

Synchronous or asynchronous: what a commit costs

With synchronous commit, the primary confirms a transaction only after the secondary has hardened the log. That is the guarantee against data loss, and its price is latency: every writing transaction waits once for the round trip to the secondary, visible as the wait type HADR_SYNC_COMMIT. With asynchronous commit, the primary doesn't wait; the secondary can fall behind, and a failover to it is possible only as a forced failover.

Single instancenot in the AGp508.2 msp9917.0 msAG synchronouswaits for the secondaryp5015.8 msp9928.7 msHADR_SYNC_COMMIT per commit: 1.17 msAG asynchronousno wait; shared lab SSDp5014.3 msp9925.5 ms0 ms10 ms20 ms30 msp50: half of the commits were faster. p99: 99 out of 100 were faster.
Single instance · not in the AGp508.2 msp9917.0 msAG synchronous · waits for the secondaryp5015.8 msp9928.7 msHADR_SYNC_COMMIT per commit: 1.17 msAG asynchronous · no wait; shared lab SSDp5014.3 msp9925.5 ms0 ms10 ms20 ms30 msp50: half of the commits were faster. p99: 99out of 100 were faster.
Commit latency per variant, median of three runs with 3,000 single-row inserts each; lab with a shared SSD, no WAN.

In the lab, we wrote to the same database three times with 3,000 single-row inserts in autocommit each: first outside the AG, then as a synchronous replica with REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT = 1, then asynchronously. We measured the latency the client sees per commit (median of three runs per variant, SQL Server 2022 CU27):

Variantp50p95p99Commits/sHADR_SYNC_COMMIT per commit
Single instance, not in the AG8.2 ms14.4 ms17.0 ms102–
AG synchronous15.8 ms25.0 ms28.7 ms561.17 ms
AG asynchronous14.3 ms20.5 ms25.5 ms680

Two things stand out. First, synchronous commit cost an additional 7.6 milliseconds per transaction here, almost a second log hardening, while the HADR_SYNC_COMMIT wait statistic showed only a little over one millisecond of that. If you are looking for the price of synchronous commit, measure the latency at the client, not just the waits. Second, asynchronous was hardly faster than synchronous in the lab. That is a lab artifact: both replicas write their log to the same SSD, and the secondary slows the primary down while hardening (our explanation, not isolated in a separate test). On separate disks, an asynchronous primary confirms right after its own log write.

What the numbers don't show: in the lab, both replicas sit on the same machine, connected by an internal Docker network. That is the latency floor of the mechanism. In practice, the link between the data centers sets the surcharge, and it applies to every single writing transaction. That is why synchronous replicas belong on a fast, short link and asynchronous ones on the long one.

One setting deserves special attention here: REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT (from 2017, default 0). It defines how many synchronous secondaries must have hardened a commit. At 0, the primary keeps working if the secondary fails and marks it as not synchronized; at that moment the guarantee against data loss is gone. At 1 or higher, commits on the primary fail as soon as too few synchronous secondaries are reachable: the data is safe, the application is down. This is not a bug but a choice between availability and protection, and it belongs before the incident, not during it. In the lab, the AG ran with the value 1; when we disconnected the secondary, the primary rejected writes with a quorum error.

How long a failover really takes: role change versus restart

Part 1 described the difference: with an FCI, the instance restarts on the other node and runs recovery for each database. With an AG, the secondary has already replayed the log; it only has to take over the role and roll back open transactions. Microsoft says "usually seconds" for the AG and "usually longer" for the FCI, but gives no number. So we measured, with one honest limitation: the lab has no Windows cluster and no FCI. As a stand-in for the FCI failover, we used a restart of the instance, because that is exactly what happens on the new node: service start and crash recovery. The cluster's detection phase is missing from both measurements; in practice it comes on top with either technology.

FCI stand-in: instance restartidle9.3 sopen 100 MB transaction, ADR off9.6 s · Undo 0.43 ssame transaction, ADR on9.8 s · Undo 0.03 sAG: role changeidle0.72 sopen transaction, required secondaries 1 · waits until the old primary is synchronized again70.6 sopen transaction, required secondaries 00.9 s · locked rows after 4 s0 s10 s20 s30 s40 s50 s60 s70 s
FCI stand-in: instance restartidle9.3 sopen 100 MB transaction, ADR off9.6 s · Undo 0.43 ssame transaction, ADR on9.8 s · Undo 0.03 sAG: role changeidle0.72 sopen transaction, required secondaries 1waits until the old primary is synchronizedagain70.6 sopen transaction, required secondaries 00.9 s · locked rows after 4 s0 s20 s40 s60 s
Until the database is writable, medians of three runs; the instance restart stands in for the FCI failover, and the cluster’s detection phase is missing in both.
What was measured (SQL Server 2022 CU27, three runs each)until the database is writableof which undo of the open transaction
Instance restart (FCI stand-in: process killed and started again), idle9.3 s (9.2–9.4)–
Restart, open transaction with 100 MB of log, ADR off9.6 s (9.6–9.6)0.43 s
Restart, same transaction, ADR on9.8 s (9.5–10.1)0.03 s
AG role change, idle0.72 s (0.71–0.78)–
Role change, open transaction with 100 MB of log, required secondaries 170.6 s (69.7–71.8)2.4 s
Role change, same transaction, required secondaries 0 (two runs)0.9 s; the rows locked by the transaction after about 4 s2.4 s

In the restart, service start dominated: about 6 seconds until the instance accepted connections, about 8 until the user database started. The open transaction cost 0.43 seconds of undo without ADR and 0.03 with ADR. At 100 MB that is small next to the service start, but without ADR the undo time grows with the transaction, while with ADR it stays small and constant (how large the difference gets with big rollbacks is shown in Accelerated Database Recovery: Rollback and Cost). A side finding: the database was writable each time before the error log reported "Recovery completed"; that is fast recovery in Developer Edition, which behaves like Enterprise. An orderly stop with SHUTDOWN was no faster than the hard kill, because the stop itself took up to four seconds.

In the role change, the new primary was writable after 0.7 seconds when idle, with no data loss in all eight runs. With an open transaction, the new primary rolled it back in 2.4 seconds but accepted no writes for 70 seconds. The cause is not the undo but the setting: with REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT = 1, the new primary waits until the old one is synchronized again as a secondary, and that took a little over a minute to resynchronize, with two database restarts (why exactly that long, we did not isolate). With the value 0, the new primary was writable after 0.9 seconds, but then ran for the same minute without a synchronized copy. Both outcomes are the same choice as in the section on commit latency: availability or protection, and it shows up in the failover, of all places.

The lesson works in both directions. For the FCI: its restart time is a recovery question. Long open transactions extend it directly, accelerated database recovery (from SQL Server 2019, also in Standard) makes the undo phase independent of them, and fast recovery (Enterprise only) brings the database online after redo while undo is still running. For the AG: the role change itself is fast, provided the secondary is synchronized and its redo has caught up. If redo is stuck, the failover waits for it, and an open transaction on the old primary has to be rolled back on the new one as well.

What each option protects against

High availability is not a state but a list of failures you have decided to protect against. For the three options, the list looks like this:

FailureFCIBasic AGFull AGLog shipping
Node (hardware, operating system, SQL service)yes, automaticyes, automatic with synchronous commityes, automatic with synchronous commitmanual
Storageno, there is only one copyyes, own copy per replica; but switching over is automatic only with DB_FAILOVER = ONyes, own copy; switching as with a basic AGyes, manual
Site (data center, network, power)only with a replicated storage solution and a node at the second siteyes, asynchronous over distance, forced failoveryes, asynchronous or as a distributed AG; forced or planned failoveryes, manual
User or application error (an accidental DELETE)no, the change is in the one copy immediatelyno, the change is replicated immediatelyno, sameyes, with a delayed restore
Damaged or suspect databaseno, the instance is still runningonly with DB_FAILOVER = ON (off by default)only with DB_FAILOVER = ONmanual
FailureFCIBasic AGFull AGLog ShippingNodeHardware, OS, serviceautomaticautomatic (sync)automatic (sync)manualStorageshared storageonly one copycopy; auto via switchcopy; auto via switchmanualSiteData center, powerreplicated storageasync, forcedforced or plannedmanualUser errore.g. a wrong DELETEin the one copyreplicated at oncereplicated at oncedelayed restoreprotected, automaticonly manual, forced or conditionalnot protected
FCIBasicAGFullAGLogshippingFailureNodeStorageSiteUsererrorprotected, automaticonly manual, forced or conditionalnot protected
What each option protects against: automatically, only manually or forced, or not at all.

Two rows deserve a second look. Storage is the limit of the FCI: if it fails or returns errors, there is nothing to switch to; Microsoft explicitly points to the storage solution for that protection. An AG has the second copy, but be careful: a storage error that hits only one database triggers no failover without DB_FAILOVER = ON, because from the cluster's point of view the instance stays healthy; the setting is off by default. And the user error is the limit of any replication: a secondary is not a backup, because it takes over the mistake within milliseconds. Only a tested backup or a delayed restore catches that; part 2 of our backup and restore series, SQL Server Restore: Points in Time and Chains, weighs that against RPO and RTO.

Reading and backing up on the secondary

The strongest argument for the full AG is that the copy does work. Readable secondaries take reports and analytics off the primary; backups on the secondary take the I/O load of the backup off it. Both exist only in Enterprise, and both have costs you should know about:

  • Readable secondary: queries run under snapshot isolation internally. For that, every changed row gets 14 bytes of overhead, on the primary as well as on the secondary, with possible page splits. Row versions and temporary statistics end up in the secondary's tempdb. Long-running read queries hold schema locks and can block the redo of DDL; the redo backlog then grows, and with it the restart time. SQL Server 2025 transfers statistics created on the secondary to the primary so that all replicas can use them.
  • Backups on the secondary: up to SQL Server 2022, regular log backups and copy-only full backups are possible there, but no differential backups. SQL Server 2025 also allows full and differential backups on any secondary. SQL Server does not enforce the AG's backup preference; your backup jobs have to evaluate it themselves with sys.fn_hadr_backup_is_preferred_replica. A backup on the secondary holds a thread on the primary, and a copy-only backup temporarily blocks log truncation on the secondary.

The basic AG can do none of this: no reading, no backups, no integrity checks on the secondary. The FCI has no second copy on which anything could run. If you want to offload reading or backing up, you have already answered the edition question.

Instance objects, jobs and logins: the daily price of an AG

An AG replicates databases. It doesn't replicate anything that belongs to the instance: logins, Agent jobs, linked servers, credentials, server configuration, server-level permissions, Resource Governor settings. After a failover, the database is there, but the application's login is missing, the maintenance job doesn't run or runs twice, and the linked server points nowhere. Operating an AG therefore means keeping these objects identical on every replica: create logins with identical SIDs, give jobs a check of whether the instance is currently the primary, and apply configuration changes everywhere.

Since SQL Server 2022 (Enterprise), the contained availability group takes over part of this: it keeps users, logins, permissions and Agent jobs in its own master and msdb databases inside the group, and they switch over with it. Jobs run there only on the primary. Connections must go through the contained AG's listener; replication, SSIS packages and maintenance plans are not supported.

The FCI doesn't have this issue. There is one instance, and it moves with everything inside it. If you have a small operations team and don't need a readable copy, don't underestimate this argument.

Maintenance: patching, rolling upgrade, failover drill

Both technologies allow maintenance without a complete outage, but in different order and with different reach.

FCI: Setup updates the nodes one after another, the passive ones first. Each updated node drops out of the list of possible owners until the instance is switched to an updated node; by default, Setup decides this once half of the nodes are done. The outage is one failover plus the run time of the upgrade scripts. What is not possible is to turn a standalone instance into an FCI afterwards, or to downgrade an FCI to a standalone instance.

AG: Secondaries first (remote first, then local), then a planned failover to a synchronized replica, and the old primary last. Beforehand: a trial failover, a full backup, an integrity check, and switching off automatic failover on the synchronous replicas. While a replica is patched higher than the primary, it can't serve readable queries or backups; set the backup preference to the primary for that time. Cluster-Aware Updating is not supported for AGs. If REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT is 1 or higher, the primary can be briefly unavailable for reads and writes during patching.

In both cases: a failover nobody has rehearsed is not protection. Plan it as a recurring drill, measure the time to the first successful write, and after an AG drill check that all replicas are synchronized again.

When a secondary stalls: log, queue and resynchronization

A secondary can stop for many reasons: a network disruption, a patch window, a disk error, or someone suspended data movement with ALTER DATABASE … SET HADR SUSPEND. The primary keeps running. But as long as the secondary database is suspended, all of its log records stay active on the primary: the transaction log can't be truncated, not even by log backups, and sys.databases shows AVAILABILITY_REPLICA as the wait reason. If the log fills up, no more changes are possible on the primary. The remedies are to make room, resume data movement, or remove the secondary database from the group. What else a full log does is covered in SQL Server Transaction Log Full: Fix Error 9002.

Secondary suspendedAfter RESUME · seconds050100150 MB0 s1 s2 stime compressedLog backup:no effectRESUMESYNCHRONIZED · 1.0–1.1 slog hardened, redo still runningQueues emptyat the latest 2.1 sPrimary’s log ~160 MBfreed only by the next log backupSecondary’s redoup to 152 MBLog send queue: NULLRPO gap 37 s (last_commit_time)61–65 % of 256 MB in use
suspendedafter RESUME050100150MB0 s1 s2 scompressed1234Primary’s log · ~160 MBSecondary’s redo queue · up to 152 MB1Log backup: no effect. The log stays61–65 % in use until the next log backupafter catching up.2RESUME3SYNCHRONIZED after 1.0–1.1 s: loghardened, redo still running.4Queues empty, at the latest after 2.1 s.While suspended: log send queue NULL, RPO gap37 s (last_commit_time).
Suspended secondary in the lab: the primary’s log grows and is not truncated; after RESUME the AG reports SYNCHRONIZED after one second, and redo needs one more.

In the lab, we reproduced exactly that: secondary suspended, about 160 MB of log generated on the primary, log backup run. The backup finished without error and freed nothing; the log stayed 61 to 65 percent full, and log_reuse_wait_desc showed AVAILABILITY_REPLICA. With REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT = 1, every write also hung until the client gave up after ten seconds. Three observations for diagnosis:

  • The log send queue was NULL while suspended, although 160 MB of log was missing. In this state it is no use as an RPO measure; the difference in last_commit_time showed the backlog correctly at 37 seconds.
  • SYNCHRONIZED means "log hardened," not "redo finished." One second after RESUME, the primary reported the secondary as synchronized while up to 152 MB of redo was still pending there. Microsoft's estimate, redo queue divided by redo rate (about 1.6 seconds), matched the measured duration; after a little over two seconds, both queues were empty.
  • The primary's view lags. What the primary knew about the secondary's redo queue was up to one second behind what the secondary itself reported. For restart time, the secondary's view is what counts.

After a forced failover, the same applies to all remaining replicas: they stay SUSPENDED until someone resumes them one by one. On resume, transactions that the new primary never confirmed are rolled back; anything in the old copy that is still worth saving has to be recovered beforehand. And a synchronous secondary that doesn't report for longer than the session timeout is marked DISCONNECTED by the primary; with REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT = 0 transactions don't wait for it, with 1 or higher they fail instead.

The application: name, listener, reconnection

With an FCI, applications connect to the instance's virtual network name; after the failover it is registered on the new node, existing connections are gone, and new ones land on the same instance. With an AG, the listener does that job: it always points to the current primary and, with ApplicationIntent=ReadOnly, can redirect readers to a readable secondary. It is not mandatory; without it, applications have to find the new primary themselves. Across several subnets, MultiSubnetFailover=True applies to both, so that the client tries all addresses in parallel.

What neither technology changes: both drop connections during a failover. An application without reconnection logic stops, whether FCI or AG. Retry with a wait, idempotent writes and a connection pool that discards dead connections are part of high availability, even if they are rarely budgeted in the database project.

Overview: Where does my availability stand right now?

The query returns, in three result sets, what this article describes: the instance (FCI or not, HADR on or off), the replicas with role and mode, and per database the log send queue, redo backlog, last_commit_time and log wait reason. On a single instance, the AG result sets stay empty, but the query still runs without error.

Query: Availability overview

Query 1: Availability overview · read-only
/* PSG Stack · FCI vs. AG · Query 1
   Availability overview: instance, cluster, availability group replicas and databases

   !!! WARNING — please read before running !!!
   - Read-only. The query changes no data and no setting, starts no failover and kills no session.
   - Use judgement on production: it only reads catalog views and HADR DMVs, which is cheap.
     Still, do not run it in a loop or every second; for continuous monitoring use the AG
     dashboard or your monitoring tool.
   - Check the output, do not copy it blindly: the values are a snapshot. Queues, rates and states
     change all the time. The columns rpo_estimate_s and rto_estimate_s are ESTIMATES based on the
     Microsoft formulas (see below), not guaranteed values.
   - Run it on the primary: only there do you see the rows of ALL replicas. On a secondary,
     part 3 shows only the local copy and the RPO estimate stays empty.
   - Permissions: VIEW SERVER STATE (from SQL Server 2022, VIEW SERVER PERFORMANCE STATE is enough).
   - MAXDOP 1: every part runs serially and uses no additional cores, so the diagnosis stays small
     even on a server that is busy right now.
   - SQL Server 2017 and later. Tested in the PSG lab on 2017, 2019, 2022 and 2025 (08 Nov 2026),
     and in an AG pair on SQL Server 2022 (CLUSTER_TYPE = NONE). On an instance without an AG,
     parts 2 and 3 return no rows and no error. A real FCI (Windows cluster) could not be tested
     in the lab.

   Result sets:
     1  Instance: version, edition, is it an FCI, is HADR enabled, which node it runs on
     2  Replicas per availability group: role, commit mode, failover mode, connection, health
     3  Databases per replica: synchronization, suspended?, queues, rates, last commit,
        estimated RPO/RTO, what the log is waiting for

   Estimation formulas (Microsoft Learn, "Monitor performance for availability groups"):
     RPO ≈ last_commit_time(primary) − last_commit_time(secondary)   (simplified variant)
     RTO ≈ redo_queue_size / redo_rate   (redo part only; detection and role change come on top,
           and with CLUSTER_TYPE = NONE there is no automatic failover at all)
     log_send_queue_size is what can be lost if the primary fails completely.
*/
SET NOCOUNT ON;

/* 1 · Instance
   is_fci = 1: failover cluster instance. fci_nodes then lists all nodes, the active one marked.
   hadr_enabled = 1: the instance may run availability groups (Always On switch).
   hadr_manager only when hadr_enabled = 1 (SERVERPROPERTY reports 1 = running otherwise, seen in the lab). */
SELECT @@SERVERNAME                                        AS server_name,
       CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS nvarchar(128)) AS active_node,
       CAST(SERVERPROPERTY('Edition') AS nvarchar(128))    AS edition,
       CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) AS version,
       CAST(SERVERPROPERTY('ProductUpdateLevel') AS nvarchar(128)) AS update_level,
       CAST(SERVERPROPERTY('IsClustered') AS int)          AS is_fci,
       CAST(SERVERPROPERTY('IsHadrEnabled') AS int)        AS hadr_enabled,
       CASE WHEN CAST(SERVERPROPERTY('IsHadrEnabled') AS int) = 0 THEN N'n/a (HADR off)'
            ELSE CASE CAST(SERVERPROPERTY('HadrManagerStatus') AS int)
                 WHEN 0 THEN N'not started, pending communication' WHEN 1 THEN N'running'
                 WHEN 2 THEN N'not started, failed' ELSE N'n/a' END END AS hadr_manager,
       (SELECT STRING_AGG(CAST(n.NodeName AS nvarchar(max))
                          + CASE WHEN n.is_current_owner = 1 THEN N' (active)' ELSE N'' END
                          + N' ' + n.status_description, N', ')
        FROM sys.dm_os_cluster_nodes AS n)                 AS fci_nodes,
       i.sqlserver_start_time                              AS instance_start
FROM sys.dm_os_sys_info AS i
OPTION (MAXDOP 1);

/* 2 · Replicas
   cluster_type: WSFC (Windows cluster), EXTERNAL (e.g. Pacemaker) or NONE (no cluster, read
   scale only, no automatic failover). required_secondaries = REQUIRED_SYNCHRONIZED_SECONDARIES_
   TO_COMMIT: this many synchronous secondaries must confirm every commit. */
SELECT ag.name                                    AS availability_group,
       ag.cluster_type_desc                       AS cluster_type,
       ag.required_synchronized_secondaries_to_commit AS required_secondaries,
       ar.replica_server_name                     AS replica,
       CASE WHEN ars.is_local = 1 THEN N'yes' ELSE N'no' END AS this_instance,
       ars.role_desc                              AS role,
       ar.availability_mode_desc                  AS commit_mode,
       ar.failover_mode_desc                      AS failover_mode,
       ar.seeding_mode_desc                       AS seeding,
       ar.secondary_role_allow_connections_desc   AS secondary_read_access,
       ars.connected_state_desc                   AS connection,
       ars.operational_state_desc                 AS operational_state,
       ars.synchronization_health_desc            AS health,
       ars.last_connect_error_description         AS last_connect_error
FROM sys.availability_groups AS ag
JOIN sys.availability_replicas AS ar
  ON ar.group_id = ag.group_id
LEFT JOIN sys.dm_hadr_availability_replica_states AS ars
  ON ars.replica_id = ar.replica_id
ORDER BY ag.name, CASE ars.role_desc WHEN N'PRIMARY' THEN 0 ELSE 1 END, ar.replica_server_name
OPTION (MAXDOP 1);

/* 3 · Databases per replica
   Units: queues in KB, rates in KB/s. suspended = 1: data movement suspended (SUSPEND); the log
   on the primary cannot be truncated then (log_reuse_wait = AVAILABILITY_REPLICA).
   failover_ready = 1: this secondary is synchronized, a failover to it loses no data.
   rpo_/rto_estimate_s: see header, estimates only.
   Seen in the lab (SQL 2022 CU27): during SUSPEND, log_send_queue_kb is NULL although log is
   missing; then last_commit / rpo_estimate_s is what counts. log_send_rate_kb_s can be implausibly
   high (average of the last active period), and SYNCHRONIZED means "log hardened", not "redo done":
   redo_queue_kb can still show hundreds of MB at that point. */
SELECT ag.name                                         AS availability_group,
       DB_NAME(drs.database_id)                        AS database_name,
       ar.replica_server_name                          AS replica,
       CASE WHEN drs.is_primary_replica = 1 THEN N'PRIMARY' ELSE N'SECONDARY' END AS role,
       drs.synchronization_state_desc                  AS synchronization,
       drs.synchronization_health_desc                 AS health,
       CAST(drs.is_suspended AS int)                   AS suspended,
       drs.suspend_reason_desc                         AS suspend_reason,
       CAST(dcs.is_failover_ready AS int)              AS failover_ready,
       drs.log_send_queue_size                         AS log_send_queue_kb,
       drs.log_send_rate                               AS log_send_rate_kb_s,
       drs.redo_queue_size                             AS redo_queue_kb,
       drs.redo_rate                                   AS redo_rate_kb_s,
       drs.last_commit_time                            AS last_commit,
       CASE WHEN drs.is_primary_replica = 0 AND p.last_commit_time IS NOT NULL
            THEN CAST(DATEDIFF(millisecond, drs.last_commit_time, p.last_commit_time) / 1000.0
                      AS decimal(12, 1)) END           AS rpo_estimate_s,
       CASE WHEN drs.is_primary_replica = 1 THEN NULL
            WHEN drs.redo_queue_size = 0 THEN CAST(0 AS decimal(12, 1))
            WHEN drs.redo_rate > 0
            THEN CAST(drs.redo_queue_size * 1.0 / drs.redo_rate AS decimal(12, 1)) END AS rto_estimate_s,
       d.log_reuse_wait_desc                           AS log_reuse_wait
FROM sys.dm_hadr_database_replica_states AS drs
JOIN sys.availability_replicas AS ar
  ON ar.replica_id = drs.replica_id
JOIN sys.availability_groups AS ag
  ON ag.group_id = drs.group_id
LEFT JOIN sys.dm_hadr_database_replica_cluster_states AS dcs
  ON dcs.replica_id = drs.replica_id
 AND dcs.group_database_id = drs.group_database_id
LEFT JOIN sys.dm_hadr_database_replica_states AS p
  ON p.group_database_id = drs.group_database_id
 AND p.is_primary_replica = 1
 AND p.is_local = 1
LEFT JOIN sys.databases AS d
  ON d.database_id = drs.database_id
 AND drs.is_local = 1
ORDER BY ag.name, database_name, CASE WHEN drs.is_primary_replica = 1 THEN 0 ELSE 1 END, replica
OPTION (MAXDOP 1);

Sample output

Sample output from the lab run (two SQL Server 2022 containers as an AG with cluster type NONE, synthetic data, names from the lab). Situation A: the secondary is suspended (suspended = 1, SUSPEND_FROM_USER, failover_ready = 0), the primary's log waits for AVAILABILITY_REPLICA, the RPO estimate shows a backlog of 36.7 seconds, the log send queue is NULL, and the send rate is an implausible average of the last active phase. The primary keeps writing here because required_secondaries was 0 for this run; with 1, writes hung (see above). Situation B: both replicas synchronized, failover_ready = 1, RPO and RTO 0, and the log waits only for the next log backup.

Sample output
Situation A · secondary suspended (HADR SUSPEND), query on the primary agp8547a7, 08 Nov 2026 19:52 UTC
=======================================================================================================

1 · Instance

server_name active_node edition                    version     update_level is_fci hadr_enabled hadr_manager fci_nodes instance_start
----------- ----------- -------------------------- ----------- ------------ ------ ------------ ------------ --------- -----------------------
agp8547a7   agp8547a7   Developer Edition (64-bit) 16.0.4295.3 CU27         0      1            running      NULL      2026-11-08 19:40:49.123

2 · Replicas

availability_group cluster_type required_secondaries replica   this_instance role      commit_mode        failover_mode seeding secondary_read_access connection operational_state health      last_connect_error
------------------ ------------ -------------------- --------- ------------- --------- ------------------ ------------- ------- --------------------- ---------- ----------------- ----------- ------------------
psg_ag             none         0                    agp8547a7 yes           PRIMARY   SYNCHRONOUS_COMMIT MANUAL        MANUAL  ALL                   CONNECTED  ONLINE            HEALTHY     NULL
psg_ag             none         0                    ags8547a7 no            SECONDARY SYNCHRONOUS_COMMIT MANUAL        MANUAL  ALL                   CONNECTED  NULL              NOT_HEALTHY NULL

3 · Databases per replica

availability_group database_name replica   role      synchronization   health      suspended suspend_reason    failover_ready log_send_queue_kb log_send_rate_kb_s redo_queue_kb redo_rate_kb_s last_commit             rpo_estimate_s rto_estimate_s log_reuse_wait
------------------ ------------- --------- --------- ----------------- ----------- --------- ----------------- -------------- ----------------- ------------------ ------------- -------------- ----------------------- -------------- -------------- --------------------
psg_ag             ag_latency    agp8547a7 PRIMARY   SYNCHRONIZED      HEALTHY     0         NULL              1              NULL              NULL               NULL          NULL           2026-11-08 19:52:26.003 NULL           NULL           AVAILABILITY_REPLICA
psg_ag             ag_latency    ags8547a7 SECONDARY NOT SYNCHRONIZING NOT_HEALTHY 1         SUSPEND_FROM_USER 0              NULL              135634000          0             80349          2026-11-08 19:51:49.283 36.7           .0             NULL

=============================================================================================================
Situation B · both replicas synchronized; after the failover runs ags8547a7 is primary, 08 Nov 2026 20:06 UTC
=============================================================================================================

1 · Instance

server_name active_node edition                    version     update_level is_fci hadr_enabled hadr_manager fci_nodes instance_start
----------- ----------- -------------------------- ----------- ------------ ------ ------------ ------------ --------- -----------------------
ags8547a7   ags8547a7   Developer Edition (64-bit) 16.0.4295.3 CU27         0      1            running      NULL      2026-11-08 19:27:22.100

2 · Replicas

availability_group cluster_type required_secondaries replica   this_instance role      commit_mode        failover_mode seeding secondary_read_access connection operational_state health  last_connect_error
------------------ ------------ -------------------- --------- ------------- --------- ------------------ ------------- ------- --------------------- ---------- ----------------- ------- ------------------
psg_ag             none         1                    ags8547a7 yes           PRIMARY   SYNCHRONOUS_COMMIT MANUAL        MANUAL  ALL                   CONNECTED  ONLINE            HEALTHY NULL
psg_ag             none         1                    agp8547a7 no            SECONDARY SYNCHRONOUS_COMMIT MANUAL        MANUAL  ALL                   CONNECTED  NULL              HEALTHY NULL

3 · Databases per replica

availability_group database_name replica   role      synchronization health  suspended suspend_reason failover_ready log_send_queue_kb log_send_rate_kb_s redo_queue_kb redo_rate_kb_s last_commit             rpo_estimate_s rto_estimate_s log_reuse_wait
------------------ ------------- --------- --------- --------------- ------- --------- -------------- -------------- ----------------- ------------------ ------------- -------------- ----------------------- -------------- -------------- --------------
psg_ag             ag_latency    ags8547a7 PRIMARY   SYNCHRONIZED    HEALTHY 0         NULL           1              NULL              NULL               NULL          NULL           2026-11-08 20:04:52.277 NULL           NULL           LOG_BACKUP
psg_ag             ag_latency    agp8547a7 SECONDARY SYNCHRONIZED    HEALTHY 0         NULL           1              0                 885353             0             347608         2026-11-08 20:04:52.277 .0             .0             NULL

Frequently asked questions

Is asynchronous commit enough for a second data center? For disaster recovery, yes, with possible data loss equal to the log send queue. There is no automatic failover to it, only a forced one.

Can I have a readable secondary in Standard? No. The basic AG allows no read access and no backups on the secondary. Readable copies are Enterprise.

Why does my log keep growing even though I run log backups? Check log_reuse_wait_desc in sys.databases. If it shows AVAILABILITY_REPLICA, a suspended or lagging secondary is holding the log.

How long does an FCI failover really take? As long as service start and recovery, and recovery takes as long as the longest open transaction. With ADR, it becomes largely independent of that. Our lab values are above; only a drill in your own environment gives you your number.

Can I run an AG without a listener? Yes. Then the automatic redirection after the failover and read-only routing are gone; the application has to find the primary itself.

Do I need fast recovery if I have ADR? Fast recovery (Enterprise) brings the database online after redo; ADR (from 2019, also Standard) makes undo nearly instant. In practice, ADR takes away the biggest time sink of recovery; fast recovery remains an additional advantage of Enterprise Edition.

What comes next

  • Part 3 · When does Enterprise pay off? Which requirement justifies the full AG, and the licensing rules behind it: passive replicas, Software Assurance, what "passive" means.
  • Part 4 · Decision aid: a few questions, reasoned candidates, no decision from three clicks.

If you're facing the decision

The matrix says what is possible. Whether it fits you is decided by your goals for data loss and recovery, storage, sites, read requirements and the team that operates it all. In the availability review of our SQL Server Health Check, we compare these goals with what your environment achieves today, including the values from the query above. For planning, build and migration, there is our SQL Server High Availability: Design and Build service. We advise without any interest in selling licenses ourselves.

Sources

The technical statements in this part rest on Microsoft documentation, checked on Nov 8, 2026: Editions and Supported Features 2017, 2019, 2022 and 2025; Monitor Performance for Always On Availability Groups; sys.dm_hadr_database_replica_states; CREATE and ALTER AVAILABILITY GROUP; Suspend and Resume an Availability Database; Perform a Forced Manual Failover; Upgrading Always On Availability Group Replica Instances; Upgrade a SQL Server Failover Cluster Instance; Failover Policy for Failover Cluster Instances; Active Secondaries (Readable Secondary Replicas, Backup on Secondary Replicas); Contained Availability Groups; What's New in SQL Server 2025; Restore and Recovery Overview (Fast Recovery); Accelerated Database Recovery. Measured values come from the PSG lab of Nov 8, 2026 on SQL Server 2022 CU27; the instance restart stands in there for the FCI failover, and a Windows cluster was not part of the lab.

More parts of the series