DBA Scripts: Get Log Shipping Status

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

Which Log Shipping Job Stopped

Log shipping copies transaction-log backups from a primary to a secondary. If the secondary falls behind or alert 14420 or 14421 fires, run this on an instance with the monitor rows: the script compares backup, copy and restore ages with their configured thresholds and shows which step needs attention. Here, status means the monitored log-shipping chain, not general server health.


Where Each Age Comes From

Log shipping is 3 SQL Server Agent jobs that fail on their own, and each one writes its own timestamp to a monitor table in msdb:

  • Backup job (primary) writes last_backup_date to log_shipping_monitor_primary. If this is old, the primary has stopped taking log backups and nothing downstream can move.
  • Copy job (secondary) writes last_copied_date to log_shipping_monitor_secondary. Old here but current on the primary means the files exist and are not arriving.
  • Restore job (secondary) writes last_restored_date to the same table. Old here but current on the copy means the files arrived and are not being applied.

Copy current and restore stale is the restore job. Copy stale as well is upstream: the backup job or the share. The verdict column makes that comparison for you, against the thresholds set when log shipping was configured, not numbers chosen by the script. Ages are worked out from the UTC columns, the same way the alert job counts them, so servers in different time zones do not skew them.


If You Arrived Holding Error 14420 or 14421

These are the 2 alerts the log shipping alert job raises, and they point at different ends of the chain:

  • 14420, “has backup threshold of N minutes and has not performed a backup log operation for N minutes”. This is the backup job on the primary. The secondary is not the problem yet; it has run out of things to restore.
  • 14421, “has restore threshold of N minutes and is out of sync. No restore was performed for N minutes. Restored latency is N minutes”. This is the secondary, but it does not say which job. MS Docs notes that a restore job which succeeds with “Could not find a log backup file that could be applied” does not update the restore time, and that the cause may then be the copy job.

So 14421 on its own is ambiguous, and copy_age_min is the column that resolves it. Read that one first.


When to Run This Script

  • The moment a 14420 or 14421 alert lands, before opening job history
  • When someone reports the reporting secondary is showing stale data
  • After any change to the backup share, the Agent service account, or the network between primary and secondary
  • After patching or restarting either instance, to confirm all 3 jobs picked back up
  • Routine SQL Server health checks on any instance that holds a log shipping role

The Script

Run it on the instance that holds the log shipping monitor role. If you are not sure which that is, run it on the secondary first.

✓ Verified
  • Tested on: SQL Server 2025 (RTM CU8) 17.0.4075.5, Windows lab instance with no log shipping configured
  • Last verified: October 2026 (a real run: the status row under Example Output). Every verdict was checked against rows in a lab copy of the 2 monitor tables; on a live pair the values follow MS Docs and were not reproduced here
  • Permissions: SELECT on the 2 monitor tables in msdb (db_datareader in msdb is enough). VIEW SERVER STATE does not help: without msdb access you get Msg 229
  • Safety: read-only, impact low
/*
Script Name : Get-LogShippingStatus
Category    : high-availability
Purpose     : Show every log shipping pair on this instance and how far behind each secondary is.
Author      : Peter Whyte (https://sqldba.blog)
Requires    : SELECT on msdb.dbo.log_shipping_monitor_primary and _secondary (db_datareader in msdb)
*/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;

/*
  WHAT THIS ANSWERS, mid-incident: "is the secondary behind, and which part of the chain
  stopped - backup, copy, or restore?"

  Log shipping is three jobs, not one, and they fail independently:
     backup  on the primary    -> msdb.dbo.log_shipping_monitor_primary.last_backup_date
     copy    on the secondary  -> log_shipping_monitor_secondary.last_copied_date
     restore on the secondary  -> log_shipping_monitor_secondary.last_restored_date

  Reading only "restore latency" tells you the secondary is behind but not WHY. If the copy
  date is current and the restore date is old, the restore job is the problem. If the copy date
  is also old, look further up: the backup job or the share. That is the whole diagnostic, and
  it is why this script prints all three ages side by side rather than one lag number.

  WHERE TO RUN IT. The monitor tables are populated on whichever instance holds the monitor
  role - often the secondary, sometimes a third server. A row appears here only for the parts
  this instance monitors, so an empty result is not proof that log shipping is healthy. It may
  mean you are on the wrong instance. The status row below says so rather than returning
  nothing and letting you conclude the wrong thing.

  THRESHOLDS ARE THE CONFIGURED ONES, not invented. backup_threshold and restore_threshold are
  the values set when log shipping was configured, in minutes. This compares against those
  rather than against a number of my choosing.

  AGES ARE UTC, the same way the alert job counts them. sys.sp_check_log_shipping_monitor_alert
  compares the *_utc columns with GETUTCDATE(), so a primary or secondary in another time zone
  from the monitor does not skew the ages. The last_* columns shown are the local times.

  LATENCY COUNTS TOO. The 14421 alert also fires when last_restored_latency (minutes from the
  log backup being taken to it being restored) passes the restore threshold, even when restores
  are running on time. The verdict checks it as well.
*/

IF NOT EXISTS (SELECT 1 FROM msdb.dbo.log_shipping_monitor_primary)
   AND NOT EXISTS (SELECT 1 FROM msdb.dbo.log_shipping_monitor_secondary)
BEGIN
    SELECT 'No log shipping rows on this instance. Either log shipping is not configured, or '
         + 'this server does not hold the monitor role - check the primary and the secondary.'
           AS status;
END
ELSE
BEGIN

    SELECT
        'PRIMARY'                                              AS role,
        p.primary_server                                       AS server_name,
        p.primary_database                                     AS database_name,
        NULL                                                   AS partner_server,
        p.last_backup_date                                     AS last_backup,
        DATEDIFF(MINUTE, p.last_backup_date_utc, GETUTCDATE()) AS backup_age_min,
        p.backup_threshold                                     AS backup_threshold_min,
        NULL                                                   AS last_copied,
        NULL                                                   AS copy_age_min,
        NULL                                                   AS last_restored,
        NULL                                                   AS restore_age_min,
        NULL                                                   AS restore_latency_min,
        NULL                                                   AS restore_threshold_min,
        CASE
            WHEN p.last_backup_date_utc IS NULL THEN 'NO BACKUP RECORDED'
            WHEN DATEDIFF(MINUTE, p.last_backup_date_utc, GETUTCDATE()) > p.backup_threshold
                 THEN 'BACKUP LATE'
            ELSE 'OK'
        END                                                    AS verdict
    FROM msdb.dbo.log_shipping_monitor_primary AS p

    UNION ALL

    SELECT
        'SECONDARY',
        s.secondary_server,
        s.secondary_database,
        s.primary_server + '.' + s.primary_database,
        NULL,
        NULL,
        NULL,
        s.last_copied_date,
        DATEDIFF(MINUTE, s.last_copied_date_utc, GETUTCDATE()),
        s.last_restored_date,
        DATEDIFF(MINUTE, s.last_restored_date_utc, GETUTCDATE()),
        s.last_restored_latency,
        s.restore_threshold,
        CASE
            WHEN s.last_restored_date_utc IS NULL THEN 'NOTHING RESTORED YET'
            -- copy current but restore stale: the restore job is the failure, not the network
            WHEN DATEDIFF(MINUTE, s.last_restored_date_utc, GETUTCDATE()) > s.restore_threshold
                 AND DATEDIFF(MINUTE, s.last_copied_date_utc, GETUTCDATE()) <= s.restore_threshold
                 THEN 'RESTORE JOB BEHIND (copy is current)'
            -- both stale: the problem is upstream of the restore
            WHEN DATEDIFF(MINUTE, s.last_restored_date_utc, GETUTCDATE()) > s.restore_threshold
                 THEN 'BEHIND - copy is stale too, check the backup job and the share'
            -- restoring on time, but each file is applied long after it was taken (14421 fires)
            WHEN s.last_restored_latency > s.restore_threshold
                 THEN 'RESTORE LATENCY OVER THRESHOLD'
            ELSE 'OK'
        END
    FROM msdb.dbo.log_shipping_monitor_secondary AS s
    ORDER BY role, database_name;

END

It returns 1 row per primary database and 1 per secondary, with each job’s age in minutes graded against its own configured threshold. When both monitor tables are empty it returns a status row instead of an empty grid.


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

# Show every log shipping pair and which job is behind:
.\run.ps1 Get-LogShippingStatus

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

This script lives in the repo at:


Example Output

With no log shipping on the instance, the whole result is one status row:

Get-LogShippingStatus on the lab instanceexample output, not something to copy
status ------ No log shipping rows on this instance. Either log shipping is not configured, or this server does not hold the monitor role - check the primary and the secondary.

An empty grid on the wrong instance looks exactly like a healthy one, which is why the script says so instead. If you get this row on a server you know is in a log shipping pair, check you are connected to the instance you think you are. The primary and secondary keep their own monitor rows, so on either of them the tables should not be empty.


Understanding the Results

Primary rows carry only the backup columns and secondary rows only the copy and restore columns, so the unused cells are NULL by design. partner_server pairs each secondary back to its primary. The columns that decide what you do next:

verdict
The ages compared against the configured thresholds: OK, BACKUP LATE, NO BACKUP RECORDED, NOTHING RESTORED YET, RESTORE JOB BEHIND (copy is current), BEHIND - copy is stale too or RESTORE LATENCY OVER THRESHOLD.Act when any row is not OK. The verdict names the job to open first, and the next section says what to check for each one.
backup_age_min
backup_threshold_min
Primary rows only. Minutes since the backup job last took a log backup, and the threshold it is judged against. MS Docs gives the backup threshold a default of 60 minutes. This is the age the 14420 alert counts.Act when the age passes the threshold. Nothing downstream can catch up until the primary takes log backups again.
copy_age_min
Secondary rows only. Minutes since the copy job last brought a file across from the backup share. This is the column that separates a restore problem from a delivery problem. The script judges it against the restore threshold, because the copy job has no threshold of its own.Act when it is stale while the primary’s backup age is current. The files exist and are not arriving: the share, the path, or the copy job’s account.
restore_age_min
restore_threshold_min
Secondary rows only. Minutes since the restore job last applied a log backup, and the restore threshold set when the secondary was added. MS Docs documents no default for it; their own example uses 45.
restore_latency_min
Secondary rows only. MS Docs: the minutes between the log backup being created on the primary and it being restored on the secondary. A configured restore delay adds straight to it. The 14421 alert fires when this passes the restore threshold too, even when restores are running on time.Act when it is over the threshold with no restore delay configured. Each file is applied long after it was taken.

NOTHING RESTORED YET and NO BACKUP RECORDED mean the configuration exists but the job has never written a timestamp. On a pair set up an hour ago that is expected. On one that has run for months, the job has never succeeded or the monitor row was reset.


What To Do With Each Verdict

BACKUP LATE. Go to the primary. Check SQL Server Agent is running and the backup job (category Log Shipping Backup) is enabled and on schedule, then read its history for the failing step. MS Docs names an invalid backup folder path and a full disk as possible causes.

RESTORE JOB BEHIND (copy is current). Stay on the secondary and open the restore job’s history. A file it cannot apply usually means someone took a log backup outside log shipping on the primary and broke the chain, or the next file in sequence is missing from the copy folder. If the job succeeds with “Could not find a log backup file that could be applied”, find the missing file before anything else; if it is gone, the secondary needs re-initialising from a fresh full backup.

BEHIND - copy is stale too. Do not start with the restore job. Check the primary row first: if it reads BACKUP LATE, fix that and the chain catches up on its own. If the primary is fine, the copy job is the fault: the share unreachable from the secondary, or the copy job’s account losing read access to it.

RESTORE LATENCY OVER THRESHOLD. Restores are running but applying old files. Check the secondary’s restore delay against its restore threshold first; if there is no delay, the restore job is working through a backlog.

Whatever the verdict, once the failing job runs again, re-run the script and watch the ages drop. The alert job only raises when a threshold is passed; it does not tell you it has recovered.


Best Practices

  • Know which server is the monitor before the incident. If the monitor role is on the primary and the primary is what just died, the monitor tables went with it.
  • Set both thresholds to at least 3 times the job frequency, as MS Docs recommends, or every schedule slip becomes an alert and the alert stops meaning anything.
  • With a deliberate restore delay, set the restore threshold above the delay, or the delayed restore reads as behind by design.
  • Keep the log backup chain to the log shipping backup job alone. A one-off log backup taken elsewhere is the classic way a healthy restore job suddenly cannot find a file to apply.

MS Docs: log_shipping_monitor_primary · log_shipping_monitor_secondary · sp_help_log_shipping_monitor · About log shipping


Related Scripts

You may also find these useful:


Frequently Asked Questions

Where do I run this, the primary or the secondary?

On the monitor server, because that is where all 3 jobs report. Per MS Docs the primary and secondary also keep their own rows in the same tables, so the primary sees its backup row and the secondary sees its copy and restore rows. If you do not know which server is the monitor, run it on the secondary first: that gives you the copy and restore ages, the half that resolves a 14421.

Log shipping is definitely configured, so why is the result empty?

First check the instance name. Per MS Docs the primary and secondary keep their own monitor rows, so on a server in the pair the tables should not be empty, and on a third server they hold rows only if it is the monitor. If the monitor was offline and has come back, its tables are not brought up to date before the alert job runs: run sp_refresh_log_shipping_monitor on the primary and the secondary. And per MS Docs, a monitor server cannot be changed once configured without removing log shipping first, so a decommissioned monitor takes the monitoring with it while the jobs keep running.

MS Docs also lists a known issue: monitoring can break when the monitor is a remote SQL Server 2025 instance and the other servers run an older version. The documented fix is to drop and recreate the log shipping configuration.

What is the difference between error 14420 and 14421?

14420 is the backup threshold on the primary: no log backup within the configured minutes. 14421 is the restore threshold on the secondary: no restore within the configured minutes, or a restore latency above it. 14420 always points at the primary’s backup job. 14421 points at the secondary, where both the copy and restore jobs live, and copy_age_min is what tells you which one.

The 14421 alert fired but the restore age is under the threshold. Why?

The alert job also checks last_restored_latency, the minutes from a backup being taken to it being restored. Restores can run every few minutes and still be applying files from an hour ago, and that fires 14421. The verdict reads RESTORE LATENCY OVER THRESHOLD for that case. The usual reason is a restore delay set higher than the restore threshold.

Is a large restore age always a problem?

Not if the secondary was set up with a restore delay on purpose. MS Docs describes it as a way to read unchanged data from the secondary after an accidental change on the primary, and the delay defaults to 0. With a delay, set the restore threshold above it, or the alert fires by design.

What permissions does this need?

SELECT on msdb.dbo.log_shipping_monitor_primary and msdb.dbo.log_shipping_monitor_secondary. A login with only CONNECT, or with VIEW SERVER STATE, gets Msg 229, Level 14, State 5, “The SELECT permission was denied on the object 'log_shipping_monitor_secondary', database 'msdb', schema 'dbo'.” db_datareader in msdb, or a grant on just those 2 tables, is enough.

sp_help_log_shipping_monitor is Microsoft’s own view of the same data, but it checks for sysadmin: anything less gets Msg 21089, “Only members of the sysadmin fixed server role can perform this operation.”


Summary

A secondary that is behind has 3 possible causes on 2 different servers. This script puts the backup, copy and restore ages side by side against their configured thresholds and names the job to open first. Run it on the monitor server the moment a 14420 or 14421 lands, and again after the fix to watch the ages fall. An empty result is the wrong instance, not a healthy pair.

Comments

Leave a Reply

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