Why Set-ADUser Sometimes Fails Right After User Creation

When working with PowerShell in Active Directory, most scripts look simple at first.
You create a user, you update some attributes, you add group memberships, and you move on.

In reality, things are not always that clean, especially in environments with multiple Domain Controllers.

I ran into a situation that looked strange at the beginning. A script created a new user successfully. Right after that, the script tried to update the same user using Set-ADUser. And suddenly, the user could not be found.

No typo. No permission issue. The user was really created. But the next command failed.

What made this confusing

Normally, when you create a user, PowerShell should continue working against the same Domain Controller. At least, that is what you would expect.

But in some cases, Set-ADUser suddenly talks to a different Domain Controller. For whatever reason, the command decides to use another DC. When that happens before replication is finished, the user simply does not exist there yet.

The result is a very confusing error. The user was created just seconds ago, but cannot be found anymore.

I want to understand exactly why this happens internally, and I will dig deeper into it. But for now, I needed a solution that works.

The idea behind the solution

The idea is simple and pragmatic.

  • Pick one Domain Controller
  • Create the user on that Domain Controller
  • Trigger replication from that same Domain Controller
  • Wait until the user is visible on all Domain Controllers
  • Only then continue with the automation

This removes guessing from the process. The script knows exactly where the change happened and makes sure the environment is in a consistent state before moving on.

Replication from the source Domain Controller

The function below triggers replication only from the Domain Controller where the change was made. This is enough, because that Domain Controller already has the latest data and only needs to push it out.

function Invoke-ADReplicationFromSourceDC {

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$SourceDC,

        [Parameter()]
        [string]$DomainDn,

        [Parameter()]
        [ValidateRange(2, 300)]
        [int]$PollSeconds = 10,

        [Parameter()]
        [ValidateRange(1, 120)]
        [int]$TimeoutMinutes = 10,

        [Parameter()]
        [switch]$Quiet
    )

    $ErrorActionPreference = 'Stop'
    $resultErrors = New-Object System.Collections.Generic.List[string]

    try {
        Import-Module ActiveDirectory -ErrorAction Stop

        $repadminPath = (Get-Command repadmin.exe -ErrorAction Stop).Source

        if (-not $DomainDn) {
            $DomainDn = (Get-ADDomain).DistinguishedName
        }

        $source = Get-ADDomainController -Identity $SourceDC -ErrorAction Stop

        $cmd = "$repadminPath /syncall $($source.Name) $DomainDn /e /A"
        Invoke-Expression $cmd | Out-Null

        $deadline = (Get-Date).AddMinutes($TimeoutMinutes)

        while ((Get-Date) -lt $deadline) {
            $meta = Get-ADReplicationPartnerMetadata -Target $source.Name -Scope Server
            if ($meta.LastReplicationResult -notcontains 0) {
                Start-Sleep -Seconds $PollSeconds
                continue
            }
            break
        }

        [pscustomobject]@{
            Status   = "ok"
            SourceDC = $source.Name
        }
    }
    catch {
        [pscustomobject]@{
            Status   = "error"
            SourceDC = $SourceDC
            Error    = $_.Exception.Message
        }
    }
}

Waiting until the user exists everywhere

Replication alone is not enough. The script also needs to know when the user is actually visible on all Domain Controllers.

This second function checks every Domain Controller and waits until the user can be found everywhere.

function Wait-ADObjectOnAllDomainControllers {

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Identity,

        [int]$PollSeconds = 3,
        [int]$TimeoutMinutes = 5
    )

    Import-Module ActiveDirectory -ErrorAction Stop

    $dcs = Get-ADDomainController -Filter *
    $deadline = (Get-Date).AddMinutes($TimeoutMinutes)

    while ((Get-Date) -lt $deadline) {
        $missing = @()

        foreach ($dc in $dcs) {
            try {
                Get-ADUser -Identity $Identity -Server $dc.Name -ErrorAction Stop | Out-Null
            }
            catch {
                $missing += $dc.Name
            }
        }

        if ($missing.Count -eq 0) {
            return "ok"
        }

        Start-Sleep -Seconds $PollSeconds
    }

    return "timeout"
}

Putting it all together

In the test section, the script discovers a writable Domain Controller, creates the user on that DC, triggers replication from the same DC, waits until the user exists on all DCs, and only then continues.

$SourceDC = (Get-ADDomainController -Discover -Writable).HostName

New-ADUser `
    -Name "Test User" `
    -SamAccountName test.user `
    -UserPrincipalName test.user@contoso.local `
    -Path "CN=Users,DC=contoso,DC=local" `
    -AccountPassword (ConvertTo-SecureString "P@ssw0rd123!" -AsPlainText -Force) `
    -Enabled $true `
    -Server $SourceDC

Invoke-ADReplicationFromSourceDC -SourceDC $SourceDC

Wait-ADObjectOnAllDomainControllers -Identity "test.user"

Why I use this pattern

This approach removed random errors from my automation. No more guessing which Domain Controller a command is using. No more “user not found” after creation.

It is not perfect, and I still want to understand why AD cmdlets sometimes switch Domain Controllers internally. But until then, this pattern works reliably and keeps my scripts predictable.

If you automate in Active Directory in environments with multiple Domain Controllers, this is a pattern worth using. Trust me 😉

Happy automating FOLKS !

Leave a comment