Type to search 66 articles.

    Practical engineering guidance

    SYSVOL stopped replicating: dirty shutdowns and content freshness

    Group Policy applies differently depending on which domain controller a client used. A read-only health check for DFSR SYSVOL, and the one recovery step that is safe to automate.

    Series: Active Directory

    • Active Directory
    • Windows Server
    • PowerShell
    • Troubleshooting

    A Group Policy change works for some users and not others. A logon script runs on Monday and not on Tuesday. A newly created policy applies in one site and is simply absent in another.

    The common cause is not Group Policy. It is SYSVOL: the domain controllers no longer hold the same content, so what a client gets depends on which one it authenticated against. That makes the symptom intermittent, user-specific and, from the service desk’s point of view, unreproducible.

    The problem: DFSR stops on purpose, and stopping is the safe choice

    DFS Replication protects the domain from propagating bad data. Two of its safety mechanisms stop replication deliberately, and both look like a failure:

    Dirty shutdown. If the DFSR database was not closed cleanly — an unexpected reboot, a service killed during shutdown, a rolled-back virtual machine snapshot — DFSR detects it on start. Event 2212 records the unexpected shutdown and the start of recovery. On Windows Server 2012 and later, event 2213 says recovery has been paused awaiting an administrative decision. Nothing replicates on that volume until somebody resumes it. Event 2214 confirms recovery finished.

    Content freshness. If a domain controller has not replicated for longer than MaxOfflineTimeInDays — 60 days by default — DFSR refuses to replicate at all and logs event 4012. This is protection against reanimating deleted content from a server that has been offline too long, and it is very commonly seen on a DC that was powered off “temporarily”.

    The worse case is event 2104: DFSR failed to recover its database on the volume and has stopped replication for every replicated folder on it.

    A note on why this recurs

    Two documented root causes are worth knowing before you fix a dirty shutdown for the third time. A DFSR service that is killed during shutdown because the service control manager’s timeout expired will produce this cycle repeatedly on a busy server. And restoring a virtual machine snapshot of a DFSR member is documented as a cause of self-perpetuating database corruption — the reason “just roll back the snapshot” is the wrong instinct for a domain controller.

    The script

    Read-only by default. With -ResumeReplication it will perform the one documented recovery step that is safe to automate, and only with an explicit acknowledgement that a backup exists.

    <#
    .SYNOPSIS
        Reports DFSR SYSVOL replication health on a domain controller.
    
    .DESCRIPTION
        Read-only by default. Checks the SYSVOL and NETLOGON shares, runs the sysvolcheck and
        advertising directory diagnostics, reports the DFSR events that stop replication
        (dirty shutdown, content freshness, database recovery failure), reads the content
        freshness threshold and reports replication state and backlog.
    
        With -ResumeReplication it calls the documented ResumeReplication method on volumes
        that logged a paused dirty-shutdown recovery. Microsoft requires a backup of the
        replicated content first, because conflict resolution during recovery can lose data,
        so the script also requires -BackupConfirmed. Supports -WhatIf.
    
        This script never performs an authoritative (D4) or non-authoritative (D2) SYSVOL
        restore. Those are disruptive, domain-wide and documented as capable of data loss;
        they are a planned change, not a script.
    
        Run on a domain controller, elevated.
    
    .PARAMETER EventDays
        How far back to read the DFS Replication log. Default 30.
    
    .PARAMETER PartnerComputerName
        Optional replication partner for a backlog count. Defaults to no backlog check.
    
    .PARAMETER ResumeReplication
        Resume replication on volumes paused after a dirty shutdown. Requires -BackupConfirmed.
    
    .PARAMETER BackupConfirmed
        Your confirmation that the replicated content on this server is backed up.
    
    .EXAMPLE
        .\Get-SysvolReplicationHealth.ps1 -Verbose
    
    .EXAMPLE
        .\Get-SysvolReplicationHealth.ps1 -ResumeReplication -BackupConfirmed -WhatIf
    
    .NOTES
        Read-only unless -ResumeReplication is supplied.
        Event 4012 (content freshness) is NOT resolved by this script: a domain controller that
        has been offline beyond the threshold needs a decision about whether its content should
        be trusted at all.
    #>
    
    #requires -Version 5.1
    #requires -RunAsAdministrator
    
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    param(
        [ValidateRange(1, 365)]
        [int]$EventDays = 30,
    
        [string]$PartnerComputerName,
    
        [switch]$ResumeReplication,
    
        [switch]$BackupConfirmed
    )
    
    $ErrorActionPreference = 'Stop'
    
    if ($ResumeReplication -and -not $BackupConfirmed) {
        throw 'Refusing to resume replication without -BackupConfirmed. Microsoft requires a backup of the replicated folders first: conflict resolution during recovery can lose data.'
    }
    
    # --- Shares -----------------------------------------------------------------------------
    $shares = foreach ($name in 'SYSVOL', 'NETLOGON') {
        $share = Get-SmbShare -Name $name -ErrorAction SilentlyContinue
        [pscustomobject]@{
            Share   = $name
            Present = [bool]$share
            Path    = $share.Path
        }
    }
    foreach ($share in $shares | Where-Object { -not $_.Present }) {
        Write-Warning "$($share.Share) is not shared on this domain controller."
    }
    
    # --- Directory diagnostics --------------------------------------------------------------
    # sysvolcheck reads the Netlogon SysVolReady state; advertising confirms the DC advertises
    # its roles. Both are read-only tests.
    $dcdiag = & dcdiag.exe /test:sysvolcheck /test:advertising 2>&1
    $dcdiagPassed = ($dcdiag | Select-String -Pattern 'passed test (SysVolCheck|Advertising)').Count -eq 2
    if (-not $dcdiagPassed) { Write-Warning 'dcdiag sysvolcheck or advertising did not pass. Review the full output in the result object.' }
    
    # --- The events that stop replication ---------------------------------------------------
    $stopEvents = @{
        2212 = 'Unexpected shutdown detected; automatic recovery started.'
        2213 = 'Recovery PAUSED after a dirty shutdown. Replication is stopped on this volume until resumed.'
        2214 = 'Dirty shutdown recovery completed.'
        2104 = 'DFSR could not recover its database. Replication stopped for all replicated folders on the volume.'
        4012 = 'Content freshness protection stopped replication: this server was offline longer than MaxOfflineTimeInDays.'
        4114 = 'SYSVOL replication membership disabled (expected during a D2/D4 procedure).'
        4602 = 'SYSVOL initialised as authoritative (D4 completed).'
        4604 = 'SYSVOL initialisation complete.'
        4614 = 'SYSVOL waiting for initial replication.'
    }
    
    $events = [System.Collections.Generic.List[object]]::new()
    try {
        Get-WinEvent -FilterHashtable @{
            LogName   = 'DFS Replication'
            Id        = $stopEvents.Keys
            StartTime = (Get-Date).AddDays(-$EventDays)
        } -ErrorAction Stop | ForEach-Object {
            $events.Add([pscustomobject]@{
                    TimeCreated = $_.TimeCreated
                    Id          = $_.Id
                    Level       = $_.LevelDisplayName
                    Meaning     = $stopEvents[$_.Id]
                    Message     = ($_.Message -split "`r?`n")[0]
                    RawMessage  = $_.Message
                })
        }
    }
    catch {
        Write-Verbose "No matching DFS Replication events in the last $EventDays days."
    }
    
    foreach ($id in 2213, 2104, 4012) {
        $matching = @($events | Where-Object Id -EQ $id)
        if ($matching.Count -gt 0) {
            Write-Warning "Event $id present ($($matching.Count) occurrence(s)): $($stopEvents[$id])"
        }
    }
    
    # --- Content freshness threshold and replication state ----------------------------------
    $maxOfflineDays = try {
        (Get-CimInstance -Namespace 'root\microsoftdfs' -ClassName DfsrMachineConfig -ErrorAction Stop).MaxOfflineTimeInDays
    }
    catch {
        Write-Warning "Could not read DfsrMachineConfig: $($_.Exception.Message)"
        $null
    }
    
    $replicationState = try { Get-DfsrState -ComputerName $env:COMPUTERNAME -ErrorAction Stop | Select-Object -First 20 }
    catch { Write-Verbose "Get-DfsrState unavailable: $($_.Exception.Message)"; $null }
    
    $backlog = $null
    if ($PartnerComputerName) {
        $backlog = try {
            # The SYSVOL replication group is hidden from the DFSR cmdlets unless asked for.
            Get-DfsrBacklog -GroupName 'Domain System Volume' -FolderName 'SYSVOL Share' `
                -SourceComputerName $PartnerComputerName -DestinationComputerName $env:COMPUTERNAME `
                -ErrorAction Stop -Verbose 4>&1 | Select-Object -First 5
        }
        catch {
            Write-Warning "Backlog check against $PartnerComputerName failed: $($_.Exception.Message)"
            $null
        }
    }
    
    # --- Optional recovery: resume a paused dirty-shutdown volume ---------------------------
    $resumed = [System.Collections.Generic.List[object]]::new()
    if ($ResumeReplication) {
        $paused = @($events | Where-Object Id -EQ 2213)
        if ($paused.Count -eq 0) {
            Write-Verbose 'No paused dirty-shutdown recovery found; nothing to resume.'
        }
        else {
            # The volume is identified in the event text; match it against the configured volumes
            # rather than resuming everything indiscriminately.
            $guidPattern = '[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}'
            $volumeGuids = $paused | ForEach-Object {
                ([regex]::Matches($_.RawMessage, $guidPattern) | ForEach-Object { $_.Value })
            } | Select-Object -Unique
    
            if (-not $volumeGuids) {
                Write-Warning 'Event 2213 found but no volume identifier could be read from it. Resume manually after reading the event.'
            }
    
            foreach ($guid in $volumeGuids) {
                $volume = Get-CimInstance -Namespace 'root\microsoftdfs' -ClassName DfsrVolumeConfig -ErrorAction Stop |
                    Where-Object { $_.VolumeGuid -like "*$($guid.Trim('{}'))*" }
    
                if (-not $volume) {
                    Write-Warning "No DFSR volume configuration matches $guid."
                    continue
                }
    
                $label = if ($volume.VolumePath) { $volume.VolumePath } else { $guid }
                if ($PSCmdlet.ShouldProcess("DFSR volume $label", 'Resume replication after dirty shutdown')) {
                    try {
                        $null = Invoke-CimMethod -InputObject $volume -MethodName ResumeReplication -ErrorAction Stop
                        $resumed.Add([pscustomobject]@{ Volume = $guid; Result = 'resume requested' })
                        Write-Verbose "Resume requested for volume $guid."
                    }
                    catch {
                        Write-Warning "Resume failed for $guid : $($_.Exception.Message)"
                        $resumed.Add([pscustomobject]@{ Volume = $guid; Result = "failed: $($_.Exception.Message)" })
                    }
                }
            }
        }
    }
    
    [pscustomobject]@{
        DomainController   = $env:COMPUTERNAME
        CollectedUtc       = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm')
        Shares             = $shares
        DcdiagPassed       = $dcdiagPassed
        DcdiagOutput       = $dcdiag
        MaxOfflineTimeDays = $maxOfflineDays
        StopEvents         = $events | Sort-Object TimeCreated -Descending | Select-Object TimeCreated, Id, Level, Meaning, Message
        ReplicationState   = $replicationState
        Backlog            = $backlog
        Resumed            = $resumed
    }

    What to do with each finding

    Event 2213, replication paused. This is the case the script can resolve. Back up the replicated content first — Microsoft is explicit that conflict resolution during recovery can lose data — then resume. If the same server does this repeatedly, treat the recurrence as the real fault and look at the service shutdown timeout, or at whether somebody is rolling back snapshots of a domain controller.

    Event 4012, content freshness. Do not simply raise MaxOfflineTimeInDays to make it go away. The protection is telling you this domain controller’s copy of SYSVOL is too old to be trusted. The decision is whether to bring its content back in line from a healthy partner — a non-authoritative synchronisation — or to demote and rebuild the server, which is often the better answer for a DC that has been offline for two months.

    Event 2104, database recovery failed. Beyond the scope of a script. Microsoft explicitly discourages deleting the DFSR database to force a rebuild.

    Shares missing, dcdiag failing, but no DFSR stop events. Look at whether SYSVOL ever initialised: events 4614 and 4604 bracket initial replication, and a DC stuck waiting for initial replication never shares SYSVOL at all.

    Why D4 and D2 are not in the script

    The authoritative and non-authoritative restore procedures work by setting msDFSR-Enabled and msDFSR-Options on the DFSR subscription object of each domain controller, in a specific order, with the DFSR service stopped domain-wide for an authoritative restore.

    They are documented, they work, and they are the correct answer to a genuinely divergent SYSVOL. They are also capable of losing Group Policy content across the domain if the wrong server is chosen as authoritative, and non-authoritative members discard their unreplicated content into a preserved folder that later syncs purge.

    That is a change with a maintenance window, a backup and a rollback plan — not a switch on a script somebody found in an article. The script reports the state that tells you whether you need it.

    Verification and limits

    The DFS Replication log channel, events 2212, 2213, 2214, 2104, 4012, 4114, 4602, 4604 and 4614, the DfsrVolumeConfig class and its ResumeReplication method with Microsoft’s backup-first requirement, MaxOfflineTimeInDays on DfsrMachineConfig with its 60-day default, the cmdlets Get-DfsrState, Get-DfsrBacklog and Get-DfsReplicationGroup, the dcdiag test names sysvolcheck and advertising, the D4/D2 attributes and procedure, the service shutdown timeout root cause and the snapshot-rollback warning were checked against current Microsoft documentation on 20 September 2026.

    The script was written for this article and statically analysed. It was not executed against a production domain controller, and the -ResumeReplication path in particular should be exercised with -WhatIf in a lab before you trust it during an incident.

    I could not confirm the meaning of DFSR event 5002 on Microsoft Learn, so it is absent from the table rather than guessed at, and I have not verified the exact command-line syntax of dfsrdiag backlog; the script uses the PowerShell cmdlet, whose parameters are documented.

    References