0010: Idle lag of ~5 minutes; an optional Agent heartbeat for less¶
Status: accepted
Date: 2026-09-28T19:03:23-03:00
Context¶
finalized_until follows the commit time of the batch's end LSN, and the end LSN is at
most sys.fn_cdc_get_max_lsn(). That LSN only moves when the capture job writes to
cdc.lsn_time_mapping: for each captured commit, and, with no changes, an idle entry
whose frequency Microsoft does not document ("entries may also be logged for which
there are no change tables entries").
The lab measured it on SQL Server 2022 CU27 (lab t1, and a probe with each scenario for
3 minutes, capture job at its defaults, pollinginterval 5 s):
| Scenario | max_lsn advances |
Worst lag of its commit time |
|---|---|---|
| idle | one every ~305 s | ~304 s |
UPDATE every 10 s on a table without CDC |
as idle | ~300 s |
CHECKPOINT every 10 s |
as idle | ~276 s |
UPDATE every 10 s on a CDC-tracked table |
every 10 s | 20 s (p50 10 s) |
So a quiet database stalls the verdict for up to ~5 minutes, and only captured commits move it sooner. The first CI runs of t7's idle check failed for this reason: they waited 4 minutes.
Decision¶
- Document the ~5-minute idle lag. It is acceptable for the default hourly periods.
- Ship
sql/heartbeat.sql, optional and run by a DBA: a one-row tabledbo.cdc_heartbeatwith CDC on, and a SQL Server Agent job that updates it every 10 seconds (the Agent's minimum interval).max_lsnthen lags by about 10 seconds. - The reader stays read-only. It does not write heartbeats itself: that would need write permission on the source and a writer on every stream, and it only works while a stream is running.
- Idle windows in the lab exceed the interval: t1 runs 11 minutes, t7's idle check 6.
Consequences¶
tests/integrationruns the shipped script and checks that an idle stream's end offset stays within 30 seconds of real time.- The heartbeat's change table grows by one row per 10 seconds (8,640 a day), removed by the normal CDC cleanup.
dbo_cdc_heartbeatis a capture instance like any other; a pipeline that fans out over every capture instance should skip it.- The idle interval is an observation on one build, not a contract: t1 keeps measuring it in CI.