DBA Scripts: Check Always On Availability Group Latency

🔧Part of the DBA-Tools Project, copy/paste SQL Server scripts and health checks.In: High Availability

How Far Behind Is Each Database, Really

A readable secondary is serving stale data, commits on the primary have slowed, or a failover is about to depend on a replica you have not checked. Run this on the primary: it returns every availability database on every replica with its send queue, redo queue and rates, so you can see which database is behind and by how much. Availability Group Replica State covers the replica-level question, connected or not.


Why Availability Group Latency Matters

  • log_send_queue_size growing faster than log_send_rate can drain it means the secondary is falling further behind over time, not just momentarily busy
  • redo_queue_size matters specifically for readable secondaries and failover time, a large redo queue means a longer wait before a failed-over secondary is actually usable
  • On a synchronous-commit replica every commit on the primary waits for the secondary to harden the log, so a slow secondary shows up first as slow commits and HADR_SYNC_COMMIT waits, while the database still reads SYNCHRONIZED and HEALTHY
  • On an asynchronous-commit replica, synchronization_health_desc reads HEALTHY whenever the database is SYNCHRONIZING, however large the queues grow. The queue sizes are the only warning

When to Run This Script

  • Routine SQL Server health checks on any AG-protected instance
  • When application teams report slow commits on a synchronous-commit AG primary
  • Before and after a planned failover, to confirm the target replica is genuinely caught up first
  • After a large batch load or index maintenance operation, to see how much log traffic it generated for replicas to absorb

The Script

Run the following script against your SQL Server instance.

✓ Verified
  • Tested on: SQL Server 2025 (RTM CU8) 17.0.4075.5, Windows lab instance, standalone with no availability group
  • Last verified: 2026-10-05 (saved output from a real run, the guard row, Get-AvailabilityGroupLatency-20261005-181659.csv). The latency query compiles on SQL Server 2025; its rows on a live AG follow MS Docs and were not reproduced here
  • Permissions: VIEW SERVER STATE, or VIEW SERVER PERFORMANCE STATE on SQL Server 2022 and later
  • Safety: read-only, impact low
/*
Script Name : Get-AvailabilityGroupLatency
Category    : high-availability
Purpose     : Display AG replica synchronization timing, queue health, and replication rates.
Author      : Peter Whyte (https://sqldba.blog/dba-scripts-check-always-on-availability-group-latency/)
Requires    : VIEW SERVER STATE
HealthCheck : Yes
*/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;

IF SERVERPROPERTY('IsHadrEnabled') = 0
    OR NOT EXISTS (SELECT 1 FROM sys.availability_groups)
BEGIN
    SELECT 'Always On Availability Groups is not enabled or no groups are configured on this instance.' AS status;
END
ELSE
BEGIN

SELECT
    ag.name                          AS ag_name,
    ar.replica_server_name,
    ars.role_desc,
    DB_NAME(drs.database_id)         AS database_name,
    drs.synchronization_state_desc,
    drs.synchronization_health_desc,
    drs.last_hardened_time,
    drs.last_redone_time,
    drs.log_send_queue_size,
    drs.log_send_rate,
    drs.redo_queue_size,
    drs.redo_rate
FROM sys.dm_hadr_database_replica_states    AS drs
INNER JOIN sys.availability_replicas        AS ar  ON ar.replica_id  = drs.replica_id
INNER JOIN sys.availability_groups          AS ag  ON ag.group_id    = ar.group_id
INNER JOIN sys.dm_hadr_availability_replica_states AS ars ON ars.replica_id = ar.replica_id
ORDER BY ag.name, database_name, ar.replica_server_name;

END

Run it on the primary. There, sys.dm_hadr_database_replica_states returns a row for each primary database and one for each of its secondary databases, so an AG with several databases gives one row per database per replica and a single lagging database stands out. On a secondary it returns only that replica’s own databases. Where Always On is not enabled, or no availability group exists, the guard at the top returns one status row instead of an error.


How To Run From The Repo

Clone DBA Tools, initialize and run the script:

# Clone dba-tools repo:
git clone https://github.com/peterwhyte-lgtm/dba-tools

# Initialize environment:
cd dba-tools
.\Initialize-Environment.ps1

# Check per-database AG sync queue size and rates:
.\run.ps1 Get-AvailabilityGroupLatency

# To run against a remote sql server:
.\run.ps1 Get-AvailabilityGroupLatency -ServerInstance SQLSERVER01

This script lives in the repo at:


Example Output

With no availability group to report, the whole result is one status row:

Get-AvailabilityGroupLatency on the lab instanceexample output, not something to copy
status ------------------------------------------------------------------------------------------ Always On Availability Groups is not enabled or no groups are configured on this instance.

If you get this row on a server that should host a replica, check SERVERPROPERTY('IsHadrEnabled'). 0 means the Always On feature is off there. 1 means the feature is on but the login that ran the script can see no availability group.


Understanding the Results

log_send_queue_size
log_send_rate
KB of log on the primary not yet sent to that secondary database, and the average KB/s the primary sent during its last active period. Queue divided by rate is roughly how long the backlog takes to send. It is not the data you would lose: MS Docs estimates that from the queue divided by the log generation rate, or from the gap in last_commit_time between the primary and secondary rows.Act when the queue climbs across 3 or 4 runs a minute apart. On a synchronous-commit replica any sustained queue is a finding, because that mode keeps it near zero.
redo_queue_size
redo_rate
KB of log in the secondary’s log files not yet redone, and the average KB/s redone. redo_rate is the log redone since the instance started divided by the time redo was actually running, so it does not say whether redo is running now. MS Docs estimates the redo part of failover time as redo_queue_size divided by redo_rate, in seconds.Act when the redo queue grows between runs while last_redone_time stops moving: redo has stalled, and a failover to that replica waits for the whole queue before the database is usable.
synchronization_state_desc
synchronization_health_desc
SYNCHRONIZED on a synchronous-commit secondary means the log is hardened there and the database is failover ready. It says nothing about redo, so a SYNCHRONIZED database can still carry a large redo_queue_size. SYNCHRONIZING is the normal, HEALTHY state of an asynchronous replica. NOT SYNCHRONIZING is NOT_HEALTHY on any replica.Act when a synchronous-commit database reads SYNCHRONIZING (PARTIALLY_HEALTHY), or any reads NOT SYNCHRONIZING. Never force a failover to a database that reads REVERTING or INITIALIZING: it cannot then be started as a primary database.
last_hardened_time
last_redone_time
On a secondary database, the time of the last hardened log block and the time the last log record was redone. On the primary database’s row, last_hardened_time is the time of the lowest hardened point across its secondaries.Act when either stops advancing between runs while the primary is still taking writes.

Best Practices

  • Watch trend, not just a single snapshot, a queue size that’s stable is very different from one that’s climbing every time you check
  • For synchronous-commit AGs, treat any sustained non-zero log_send_queue_size as worth investigating, that mode exists specifically to keep this near zero
  • Check redo_queue_size specifically before relying on a readable secondary for reporting, or before a planned failover to that replica
  • Pair with Availability Group Replica State, connection health and queue latency are two different failure modes that can occur independently

MS Docs: sys.dm_hadr_database_replica_states · sys.dm_hadr_availability_replica_states · sys.availability_groups · Monitor performance for availability groups


Related Scripts

You may also find these scripts useful:


Frequently Asked Questions

How many seconds behind is the secondary?

This script returns KB, not seconds. For the time a failover would wait on redo, divide redo_queue_size by redo_rate, and treat it as rough because the rate is a lifetime average. For data at risk, run on the primary and compare last_commit_time on the primary database’s row with the secondary’s row, or read secondary_lag_seconds (SQL Server 2016 and later). It reads 0 while data movement is suspended, so 0 on a suspended database is not good news. Neither column is in this script’s output; both are in the same DMV.

Is a non-zero redo_queue_size always a problem?

Not necessarily. Some redo lag is normal on a busy asynchronous replica. What matters is whether it is stable or shrinking (the secondary is working through a queue) or growing without bound (the secondary cannot keep pace with the primary’s log generation).

Why would log_send_rate be healthy but redo_queue_size still growing?

Log can be received faster than it can be replayed on the secondary if that replica has less I/O or CPU headroom than the primary. Sending isn’t the bottleneck in that case, applying the log is.

Why does it show only some databases when I run it on a secondary?

Because a secondary only reports its own databases. The primary is the only replica that returns a row for every secondary database. Find the primary with Availability Group Replica State and run the script there.

What permission does it need?

VIEW SERVER STATE, or VIEW SERVER PERFORMANCE STATE on SQL Server 2022 and later. Without it the latency query fails with Msg 262, Level 14, State 1, “VIEW DATABASE PERFORMANCE STATE permission denied in database ‘master’.” Granting VIEW DATABASE PERFORMANCE STATE in master, as that message suggests, is not enough: the query then fails with Msg 300 asking for VIEW SERVER PERFORMANCE STATE.


Summary

Replica connection state answers whether an Availability Group is broadly healthy. This script answers how far behind each database is, in KB of log waiting to be sent and waiting to be redone. Run it a few times a minute apart: a queue that climbs between runs is falling behind, one that shrinks is catching up.

Run this alongside Availability Group Replica State as routine health checks, and specifically before trusting a secondary for reporting or a planned failover.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *