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.
In short
- 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.
- 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. - 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.
- 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_COMMITcan turn a one-second failover into a minute without write access. - A stalled secondary is the primary's problem: as long as it is suspended, the primary's log can't be truncated.
- 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.
| Feature | FCI | Basic AG | Full AG |
|---|---|---|---|
| Edition | Std (2 nodes), Ent (up to 16 nodes) | Std only | Ent only |
| Protection level | instance | one database per group | a group of databases |
| Data copies | one, on shared storage | two | one per replica, up to nine replicas |
| Synchronous / asynchronous | not applicable | both possible | both; several synchronous replicas (number depends on the version, see below) |
| Automatic failover | yes, through the WSFC policy | yes, with synchronous commit | yes, with synchronous commit and a synchronized target; 2025: fast failover with RestartThreshold 0 |
| Readable secondary | no, passive nodes are stopped | no | yes; 2025 with persisted statistics |
| Backups on the secondary | not applicable | no, no integrity checks either | log and copy-only full backups; 2025 also full and differential backups |
| Automatic seeding | not applicable | yes | yes (since 2016; the T-SQL default is MANUAL) |
| Distributed AG | can host a replica of a distributed AG, manual failover only | no | yes, Ent; 2025 also with contained AGs |
| Contained AG (logins and jobs inside the group) | not applicable | no | from 2022, Ent |
Without a cluster (cluster type NONE) | no | from 2019 (2017: Ent only, as read-scale) | from 2017, read scale and migration only, no high availability |
| Listener / name | virtual network name, always | listener optional | listener optional; without it, no redirection and no read-only routing |
| Query Store on the secondary | not applicable | no | 2022 only with a trace flag, 2025 as a preview, not on by default |
| What goes along in a failover | the whole instance | only the database | only the databases of the group (contained AG: plus logins and jobs) |
| A third option for comparison | Log 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_sizedivided byredo_rate, both insys.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_sizeis 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 inlast_commit_timebetween 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.
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):
| Variant | p50 | p95 | p99 | Commits/s | HADR_SYNC_COMMIT per commit |
|---|---|---|---|---|---|
| Single instance, not in the AG | 8.2 ms | 14.4 ms | 17.0 ms | 102 | – |
| AG synchronous | 15.8 ms | 25.0 ms | 28.7 ms | 56 | 1.17 ms |
| AG asynchronous | 14.3 ms | 20.5 ms | 25.5 ms | 68 | 0 |
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.
| What was measured (SQL Server 2022 CU27, three runs each) | until the database is writable | of which undo of the open transaction |
|---|---|---|
| Instance restart (FCI stand-in: process killed and started again), idle | 9.3 s (9.2–9.4) | – |
| Restart, open transaction with 100 MB of log, ADR off | 9.6 s (9.6–9.6) | 0.43 s |
| Restart, same transaction, ADR on | 9.8 s (9.5–10.1) | 0.03 s |
| AG role change, idle | 0.72 s (0.71–0.78) | – |
| Role change, open transaction with 100 MB of log, required secondaries 1 | 70.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 s | 2.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:
| Failure | FCI | Basic AG | Full AG | Log shipping |
|---|---|---|---|---|
| Node (hardware, operating system, SQL service) | yes, automatic | yes, automatic with synchronous commit | yes, automatic with synchronous commit | manual |
| Storage | no, there is only one copy | yes, own copy per replica; but switching over is automatic only with DB_FAILOVER = ON | yes, own copy; switching as with a basic AG | yes, manual |
| Site (data center, network, power) | only with a replicated storage solution and a node at the second site | yes, asynchronous over distance, forced failover | yes, asynchronous or as a distributed AG; forced or planned failover | yes, manual |
User or application error (an accidental DELETE) | no, the change is in the one copy immediately | no, the change is replicated immediately | no, same | yes, with a delayed restore |
| Damaged or suspect database | no, the instance is still running | only with DB_FAILOVER = ON (off by default) | only with DB_FAILOVER = ON | manual |
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.
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
NULLwhile suspended, although 160 MB of log was missing. In this state it is no use as an RPO measure; the difference inlast_commit_timeshowed the backlog correctly at 37 seconds. SYNCHRONIZEDmeans "log hardened," not "redo finished." One second afterRESUME, 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
/* 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.
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 NULLFrequently 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.