Skip to main content
High AvailabilitySQL ServerAlways Onavailability groupsHADRRPOsynchronous commitfailoverreplication

SQL Server Always On: Why Synchronous Commit Does Not Mean RPO = Zero

Synchronous commit prevents data loss only while the secondary is SYNCHRONIZED. When it falls out of sync, the primary commits without waiting.

12 min read

SQL Server Always On: Why Synchronous Commit Does Not Mean RPO = Zero

The availability group is synchronous commit. The failover completes cleanly. The DBA writes RPO = 0 in the post-incident report. The claim is repeated in the DR architecture document. It appears in the board-level resilience summary.

Six months later, an unplanned primary failure during a network event loses 38 seconds of committed transactions. The post-mortem reveals that the secondary was not synchronized at the time of the failure. The architecture document was describing a configuration, not a guarantee.

The answer

Synchronous commit prevents the primary from acknowledging a transaction commit to the client until the secondary has hardened the log to disk — but only while the secondary is actively synchronized. When the secondary falls out of the SYNCHRONIZED state, the primary continues accepting and acknowledging commits without waiting for the secondary at all. If the primary fails during that window, transactions committed on the primary but not yet received by the secondary are lost on failover. RPO = 0 requires the secondary to be in the SYNCHRONIZED state continuously — not just synchronous commit to be configured.

What you will learn

  • Why synchronous commit mode and the SYNCHRONIZED synchronization state are two separate things
  • What causes a secondary to fall out of SYNCHRONIZED state while the primary keeps running
  • Which failover operations carry data loss risk regardless of commit mode configuration
  • How to monitor actual RPO exposure in real time using documented DMVs

Scope: SQL Server 2016–2022 Always On Availability Groups, on-premises and Azure VMs. REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT behaviour is specific to SQL Server 2017 and later. Behaviour varies by AG topology (number of replicas, quorum configuration, WSFC settings) and network environment. Validate all observations against your specific environment.

How synchronous commit works when everything is healthy

When a secondary replica is connected and in the SYNCHRONIZED state, the commit path on the primary follows this sequence:

1. Application calls COMMIT
2. Primary hardens log records to local disk (log write)
3. Primary sends log block to secondary over the mirroring endpoint
4. Secondary hardens log records to its local disk
5. Secondary sends acknowledgment back to primary
6. Primary acknowledges COMMIT to the application

Step 6 does not happen until step 5 completes. The client never receives a successful commit acknowledgment unless the secondary has already hardened those records. For any transaction that the application knows committed, the data is on both replicas.

This is the condition under which RPO = 0 holds for committed transactions.

The state that actually matters: SYNCHRONIZED vs SYNCHRONIZING

SQL Server tracks synchronization state per database per replica in sys.dm_hadr_database_replica_states. The column synchronization_state_desc has three relevant values:

State Meaning Primary commits wait for secondary?
SYNCHRONIZED Secondary has applied all log up to the primary’s last hardened LSN Yes
SYNCHRONIZING Secondary is applying log but has not yet caught up No
NOT SYNCHRONIZING Secondary is disconnected or suspended No

Only SYNCHRONIZED enforces the commit hold. When a synchronous secondary transitions to SYNCHRONIZING or NOT SYNCHRONIZING, the primary immediately stops waiting for it and begins acknowledging commits unilaterally. The commit mode setting does not change — the configuration still reads SYNCHRONOUS_COMMIT — but the synchronization guarantee is no longer active.

Check current state across all replicas:

SELECT
    ar.replica_server_name,
    adc.database_name,
    ar.availability_mode_desc,
    drs.synchronization_state_desc,
    drs.synchronization_health_desc,
    drs.log_send_queue_size,
    drs.redo_queue_size,
    drs.last_commit_time
FROM sys.dm_hadr_database_replica_states AS drs
JOIN sys.availability_replicas AS ar
    ON drs.replica_id = ar.replica_id
JOIN sys.availability_databases_cluster AS adc
    ON drs.group_database_id = adc.group_database_id
ORDER BY ar.replica_server_name, adc.database_name;

A synchronous secondary that is not in SYNCHRONIZED state represents an open RPO exposure. How large that exposure is depends on how long it has been out of sync and how much transaction volume the primary has processed in that window.

To query specifically for synchronous secondaries that are not synchronized:

SELECT
    ar.replica_server_name,
    adc.database_name,
    drs.synchronization_state_desc,
    drs.log_send_queue_size   AS unsent_log_kb,
    drs.redo_queue_size       AS unapplied_log_kb
FROM sys.dm_hadr_database_replica_states AS drs
JOIN sys.availability_replicas AS ar
    ON drs.replica_id = ar.replica_id
JOIN sys.availability_databases_cluster AS adc
    ON drs.group_database_id = adc.group_database_id
WHERE ar.availability_mode_desc = 'SYNCHRONOUS_COMMIT'
  AND drs.synchronization_state_desc <> 'SYNCHRONIZED'
  AND drs.is_local = 0;

Any rows returned mean the primary is currently committing without a secondary acknowledgment. The log_send_queue_size value (in KB) is the current data loss exposure if the primary fails right now.

What causes a secondary to fall out of SYNCHRONIZED state

The transition from SYNCHRONIZED to SYNCHRONIZING or NOT SYNCHRONIZING can happen for reasons that are transient, intermittent, and not always visible in application monitoring:

  • Network interruption or instability between primary and secondary, even briefly. The AG endpoint uses TCP; a momentary packet loss or latency spike that exceeds the session timeout triggers a disconnect.
  • Secondary under I/O pressure: if the secondary’s redo thread cannot apply log fast enough to keep pace with the primary’s log generation rate, the secondary falls behind. The state transitions from SYNCHRONIZED to SYNCHRONIZING.
  • Secondary restart or patching: any restart of the secondary SQL Server instance or Windows host takes the replica offline temporarily. Depending on how long the restart takes and how much log the primary generates, the secondary may rejoin as SYNCHRONIZING rather than immediately SYNCHRONIZED.
  • Log shipping suspension: a DBA manually suspending data movement on the secondary (ALTER DATABASE ... SET HADR SUSPEND) immediately transitions the database out of synchronized state.
  • Storage latency on the secondary: the synchronization guarantee depends on the secondary hardening the log quickly. If secondary storage is slow or under contention, redo throughput drops and the replica falls behind.

None of these cause an alert by default. The AG health dashboard in SSMS and the synchronization health alert in SQL Server Agent require configuration. Without monitoring synchronization_state_desc continuously, a secondary can spend hours in SYNCHRONIZING while the primary accumulates data loss exposure silently.

Three failure scenarios where data loss occurs

Scenario 1: Primary fails while secondary is out of sync

The secondary loses connectivity briefly due to a network event. The primary transitions the secondary to NOT SYNCHRONIZING and continues accepting transactions. The network recovers and the secondary starts catching up. Before the secondary reaches SYNCHRONIZED state again, the primary crashes. The secondary is promoted. The transactions committed on the primary after the secondary disconnected are gone.

The data loss window is the volume of log generated on the primary between the moment of disconnect and the moment of primary failure.

Scenario 2: Forced failover on an unsynchronized secondary

The primary becomes unavailable. The secondary is in SYNCHRONIZING state with a non-zero log_send_queue_size. The operator needs to restore service and issues:

ALTER AVAILABILITY GROUP [AGName] FORCE_FAILOVER_ALLOW_DATA_LOSS;

This command explicitly accepts data loss as the trade-off for availability. It bypasses the synchronization requirement. It is the correct command to use when the primary is unrecoverable and the secondary is the only option — but it is not RPO = 0. Every transaction in the send queue at the time of the command is lost.

This is documented behaviour, not a bug. Using FORCE_FAILOVER_ALLOW_DATA_LOSS during an emergency is often the right operational decision. The problem arises when the DR plan states RPO = 0 and then uses this command without acknowledging what it does.

Scenario 3: Commit mode changed for a maintenance window and not restored

A DBA switches the secondary to asynchronous commit before a maintenance operation to avoid the performance impact of cross-site synchronous commit:

ALTER AVAILABILITY GROUP [AGName]
MODIFY REPLICA ON N'SecondaryServer'
WITH (AVAILABILITY_MODE = ASYNCHRONOUS_COMMIT);

The maintenance completes. The mode is not switched back. The AG continues operating in asynchronous mode. Weeks later, an unplanned failover produces data loss. The architecture document still says synchronous commit.

Automatic failover is protected — manual failover is not

Automatic failover in an Always On AG requires the secondary to be in the SYNCHRONIZED state. If the secondary is not synchronized when the primary becomes unavailable, automatic failover does not trigger — the cluster withholds the failover rather than risk data loss. This means scenarios 1 and 3 above typically require a human to initiate the failover manually. The human making that decision is the point where data loss gets accepted or avoided. FORCE_FAILOVER_ALLOW_DATA_LOSS is explicit about the trade-off; a manual planned failover (ALTER AVAILABILITY GROUP ... FAILOVER) will refuse to proceed if the secondary is not synchronized.

REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT (SQL Server 2017+)

SQL Server 2017 introduced the REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT option on availability groups. This parameter controls the minimum number of synchronous secondaries that must acknowledge a log block before the primary acknowledges the commit to the client.

ALTER AVAILABILITY GROUP [AGName]
SET (REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT = 1);

With this set to 1 in a two-node synchronous AG, the primary will refuse to commit if the secondary is not synchronized. The primary blocks rather than degrade to unacknowledged commits. This provides a genuine RPO = 0 guarantee for committed transactions — but at the cost of primary availability. If the secondary becomes unavailable, the primary stops accepting writes until the secondary returns or the setting is changed.

The default value is 0, which means the primary never blocks on secondary acknowledgment — it continues committing unilaterally when the secondary is unavailable. This is the setting that allows the RPO = 0 misconception to persist undetected.

In SQL Server 2019 and 2022, this option is surfaced in the AG wizard as Required synchronized secondaries to commit. It remains 0 by default.

Before setting this to 1, confirm that the production workload can tolerate primary write unavailability during secondary downtime. For most OLTP systems, this is not acceptable. The right answer depends on whether RPO or availability is the higher priority — and that is a business decision, not a DBA configuration choice.

Monitoring actual RPO exposure

Point-in-time queries against DMVs answer whether the secondary is synchronized right now. For continuous monitoring, alert on:

-- Alert condition: synchronous secondary not synchronized
SELECT
    ar.replica_server_name,
    adc.database_name,
    drs.synchronization_state_desc,
    drs.log_send_queue_size AS data_loss_exposure_kb
FROM sys.dm_hadr_database_replica_states AS drs
JOIN sys.availability_replicas AS ar
    ON drs.replica_id = ar.replica_id
JOIN sys.availability_databases_cluster AS adc
    ON drs.group_database_id = adc.group_database_id
WHERE ar.availability_mode_desc = 'SYNCHRONOUS_COMMIT'
  AND drs.is_local = 0
  AND drs.synchronization_state_desc <> 'SYNCHRONIZED';

Any result from this query means the AG is currently outside the RPO = 0 condition. The data_loss_exposure_kb value is the size of the exposure at that instant.

Also monitor:

  • sys.dm_hadr_availability_replica_states.synchronization_health_desc — aggregate health across all databases on a replica; values are HEALTHY, PARTIALLY_HEALTHY, and NOT_HEALTHY.
  • Windows Event Log and SQL Server error log for AG state change events (event IDs 35206, 35207, 35264).
  • AG dashboard in SSMS for a visual summary, noting that it reflects a point-in-time snapshot, not a continuous view.

SQL Server Agent alerts on category Always On Availability Groups can be configured to fire on state transitions. These should be standard in any environment where the DR plan references an RPO figure.

Mitigation hierarchy

  1. Establish a baseline: query synchronization_state_desc for all synchronous secondaries and understand how frequently they leave the SYNCHRONIZED state and for how long. The current state of your RPO claim depends on this.
  2. Alert on synchronization state transitions: create SQL Server Agent alerts or an external monitoring check that fires when a synchronous secondary is not synchronized. Do not rely on the SSMS dashboard for continuous visibility.
  3. Decide whether to use REQUIRED_SYNCHRONIZED_SECONDARIES_TO_COMMIT: if the application and business requirement genuinely requires RPO = 0, evaluate setting this to 1. If primary write availability during secondary downtime is not acceptable, the RPO = 0 requirement needs to be revisited with the business.
  4. Validate the failover runbook: confirm whether FORCE_FAILOVER_ALLOW_DATA_LOSS appears in the runbook. If it does, confirm whether the DR plan explicitly acknowledges the data loss that command produces. If the plan states RPO = 0 and includes this command, the plan is internally inconsistent.
  5. Document what RPO = 0 actually means in your environment: the claim is only valid when the secondary is SYNCHRONIZED. Document the conditions and the monitoring that confirms them, rather than stating it as an unconditional architectural property.
  6. Review after maintenance operations: after any secondary restart, patching, or mode change, confirm the secondary has returned to SYNCHRONIZED before closing the maintenance window.

Do not do this

  • Do not state RPO = 0 in DR documentation based solely on the availability mode configuration. The synchronization state at the time of failure determines actual data loss, not the mode setting.
  • Do not use FORCE_FAILOVER_ALLOW_DATA_LOSS without understanding that the name is literal. Every KB of log_send_queue_size at the time of the command is lost.
  • Do not switch to asynchronous commit for maintenance and rely on a change ticket to switch back. Automate the reversion or include an explicit verification step.
  • Do not treat the absence of AG alerts as confirmation that the secondary is synchronized. Default alerting does not cover synchronization state degradation.

Official references

Conclusion

The architecture document was not wrong about the commit mode. It was wrong about what that commit mode guarantees when the secondary is not synchronized. Synchronous commit is a mechanism that eliminates data loss under a specific condition — both replicas connected and the secondary in SYNCHRONIZED state. It does not eliminate data loss under all conditions, and it provides no guarantee when that condition is not met.

The RPO figure in a DR document should be a measured, monitored claim, not an inference from a configuration setting. Know what synchronization_state_desc reads right now. Alert when it changes. Understand what FORCE_FAILOVER_ALLOW_DATA_LOSS does before it appears in an emergency runbook.

Continue reading


Marios Pavlidis Principal Database Administrator