HomeNewsletterCommunityToolsArchiveBlogToday's NewsAboutServicesQuick Links Subscribe free
← Back to Blog

Your Entra Cloud Sync Job Has Been Failing Silently — Here's How to Catch It

IA
Imran Awan
22 July 2026

A quick scope note before anything else: this post is about Microsoft Entra Cloud Sync — the modern, lightweight-agent-based sync tool that Microsoft has been pushing as the default for new hybrid tenants. It is not about classic Microsoft Entra Connect Sync (the older, full on-premises sync engine that most established tenants still run). The reason for the split is simple: Cloud Sync exposes its job status through a clean, documented Microsoft Graph API surface that you can query, script, and alert on. Classic Entra Connect Sync's health data lives in an on-premises database and a desktop management tool, with no equivalent Graph read surface. Everything below is grounded in the API that actually exists for Cloud Sync — nothing here is a guess about how the older product behaves internally.

With that out of the way: hybrid identity sync failures are unusually good at hiding. A Conditional Access gap or an over-privileged role at least shows up if someone thinks to look for it. A sync failure is worse, because there is no natural moment where anyone thinks to look — the job usually keeps running, keeps reporting "success" at the job level, and the actual damage is scoped to one or two objects that nobody is watching. Below: exactly how that happens, how to manually confirm one job's real status before you trust any script's output, and a complete PowerShell audit that surfaces every Cloud Sync job with a rising failure count across your whole tenant.

The problem: a name change, a duplicate attribute, and three weeks of silence

Here's the scenario, and if you run hybrid identity for more than a handful of users you will recognise it immediately. An employee gets married and updates her surname in the on-premises HR system, which flows into Active Directory as a normal attribute change. Nothing unusual about that — it happens every week in a mid-sized organisation. Entra Cloud Sync picks up the change on its next scheduled cycle and tries to push the update to the matching object in Entra ID.

Except this time, the update collides with something. Maybe the attribute Cloud Sync uses to match the on-premises object to its Entra ID counterpart — commonly something like mail, userPrincipalName, or proxyAddresses, depending on how the sync rules are configured — now points at a value that already exists on a different object. A leftover disabled account from a previous employee with a similar name, a stale guest invitation, a duplicate mailbox created during a migration years ago. Whatever the cause, Cloud Sync now has two objects claiming the same identifying value, and it cannot safely decide which one should win. It doesn't crash. It doesn't email anyone. It just fails to provision that one object, logs the failure internally, and moves on to the next object in the batch.

Renamed user

The employee's Entra ID profile keeps her maiden name. Her email signature, her Teams profile card, and anywhere her display name is looked up all stay stale — because the specific object update keeps failing, cycle after cycle, while the job as a whole reports as running normally.

Duplicate attribute conflict

Two on-premises objects now resolve to the same matching value in Entra ID. Cloud Sync cannot merge them and won't guess — it skips the update and waits for a human to resolve the underlying duplicate on-premises.

OU moved out of scope

Someone reorganises the on-premises OU structure for an unrelated reason — a departmental restructure, a naming cleanup — and a whole branch of users quietly falls outside the OU scoping filter configured in the Cloud Sync configuration. Those users simply stop being evaluated at all.

Three weeks later, a helpdesk ticket lands: "Why does Teams still show my old name?" or "Why is my manager showing as someone who left the company two years ago?" The helpdesk agent checks the user's profile in the portal, sees it's wrong, and has no idea why — because nothing about the tenant looks broken. Licensing is fine. Sign-in works. Conditional Access is unaffected. The only place the failure is actually visible is inside the Cloud Sync job's own execution history, which nobody was watching, because nothing prompted anyone to look.

Note: There is no Group Policy or Intune CSP involved anywhere in this post. Entra Cloud Sync is a pure cloud provisioning capability — the sync agent on-premises talks outbound to the Microsoft Graph-backed provisioning service, and everything about a job's health, history, and error detail lives entirely in Entra ID. If you're looking for a device policy path here, there isn't one; the entire audit surface is the Graph API described below.

Why it happens: sync jobs retry quietly, and the retry counter can mislead you

Before going further, three terms worth defining plainly, because the rest of this post leans on them constantly. A sync job is the scheduled, repeating task that walks through objects in scope and pushes creates, updates, and deletes between the source and the target. Provisioning is the general term for that create/update/delete work — the actual act of making an object in the target system match the source. A connector is the piece of configuration that defines one side of that relationship — in Cloud Sync's case, one connector talks to on-premises Active Directory (the source) and another represents Entra ID (the target).

Cloud Sync jobs are exposed through Microsoft Graph as a synchronization resource that sits underneath the service principal representing your Cloud Sync configuration — this is the same "Application Provisioning" API area that Microsoft Graph uses for provisioning connectors generally, and Cloud Sync reuses it under the hood. The cmdlet you'll use throughout this post is:

PowerShell 7 — inspecting the raw job object
# Requires the Microsoft.Graph.Applications module and Synchronization.Read.All
$job = Get-MgServicePrincipalSynchronizationJob -ServicePrincipalId $spId
$job.Status | Format-List *

The object that comes back carries a Status property, and inside it a LastExecution block — this is the part worth studying closely, because it's where the actual health signal lives:

Raw Graph object — $job.Status.LastExecution
State                              : Failed
TimeBegan                          : 07/01/2026 02:14:07
TimeEnded                          : 07/01/2026 02:19:41
CountEntitled                      : 4812
CountEntitledForProvisioning       : 4812
CountSuccessiveCompleteFailures    : 14
Steps                              : {Import, Match, Determine Provisioning Action, Apply Update}
  Steps[3].SynchronizedEntryCount        : 4809
  Steps[3].SynchronizedEntryCountFailed  : 3

CountSuccessiveCompleteFailures is the number to watch first. It increments every time an entire job run fails outright — not every time one object fails, but every time the whole cycle doesn't complete. A job that has been sitting at 14 consecutive complete failures has been trying and failing for roughly 14 scheduled cycles straight, with nobody intervening. That is exactly the "nobody noticed" signal this post is about: the number only grows when there is genuinely no human paying attention, because a single glance at the portal would prompt someone to act.

But notice what it does not tell you in the example above: the job itself hasn't completely failed in the "whole cycle collapsed" sense in this particular run — it processed 4,809 of 4,812 entitled objects successfully and only 3 failed at the final apply-update step. That's the renamed-user, duplicate-attribute scenario from the previous section in miniature: a job can be "mostly fine" at the aggregate level while specific objects are silently and repeatedly failing underneath it.

Gotcha: CountSuccessiveCompleteFailures resets to zero the moment the job completes one full successful run — even if the specific objects that were failing before are still failing. A job can go from "14 consecutive complete failures" to "0" purely because the overall run technically completed, while the same duplicate-attribute conflict from three weeks ago is still sitting there unresolved on the same one or two objects. A reading of 0 tells you the job engine is healthy. It tells you nothing about whether every individual object synced cleanly — for that you need the per-step failure counts inside Steps, or the object-level error list in the portal covered in the next section.

How to verify: check one job manually before you trust the script

Before running anything against your whole tenant, spend two minutes confirming what one specific job actually looks like in the Entra admin center. This matters for two reasons: it gives you a sanity check that the script's numbers match reality, and the portal shows a per-object error tab that the summary-level Graph fields above don't fully replace.

  1. Sign in to the Microsoft Entra admin center with a role that can read hybrid management settings (Hybrid Identity Administrator or Global Administrator)
  2. Browse to Identity › Hybrid management › Microsoft Entra Connect › Cloud sync
  3. Select the specific Cloud Sync configuration you want to inspect (most tenants only have one, but larger environments with multiple AD forests may have several)
  4. Open the Provisioning tab for that configuration and review the current cycle status, the statistics panel, and — critically — the object-level error list underneath it
  5. Note down what you see: overall status, last run time, and the count and nature of any per-object errors, so you have something concrete to compare the script's output against
Entra admin center — Provisioning tab, current cycle
Current cycle status
Last run 07/01/2026 02:19 · Quarantine risk: elevated
Failing
a.h████@contoso.com
AttributeValueMustBeUnique — proxyAddresses conflict
Object error
Objects processed this cycle
4,809 succeeded · 3 failed
4,812 total

That object error — a uniqueness conflict on an attribute the sync rule uses for matching — is the smoking gun for the duplicate-attribute scenario described earlier. Once you've confirmed one job's status this way and it lines up with what Graph reports for the same job, you can trust the script's tenant-wide output for everything else.

Don't: restart or reset a failing sync job as a first response before you understand what's actually failing. A reset can force a full re-provisioning pass across every object in scope, which on a tenant with several thousand objects can take hours, and if the underlying conflict hasn't been resolved on-premises first, you'll just get the same failure again — except now you've also burned a full re-sync cycle and potentially disrupted objects that were syncing fine. Diagnose the specific object error first. Fix the root cause — usually on the source side, in Active Directory — then let the job run its next scheduled cycle or trigger it deliberately once you know the fix is in place.

The fix: Get-CloudSyncHealthReport.ps1

Manually opening the Provisioning blade for every Cloud Sync configuration, on every check-in, doesn't scale past a handful of tenants or a handful of weeks. The script below uses the Microsoft Graph PowerShell SDK to do the same check programmatically, across every service principal in the tenant that has an active synchronization configuration.

Deliberately, it does not assume a specific display-name pattern for the Cloud Sync service principal. Naming can vary between tenants and Microsoft has changed default naming conventions before, so guessing a string like "On-premises directory synchronization" and filtering on it risks silently skipping a real configuration that happens to be named differently. Instead, the script enumerates service principals and asks each one, generically, whether it has any synchronization jobs at all — which is a safe, tenant-agnostic way to find every Cloud Sync (and any other Graph-based provisioning) configuration that exists.

Download the script — GitHub

Script is written and syntax-validated locally; it will be tested on a live tenant before it's pushed. Once pushed, download Get-CloudSyncHealthReport.ps1 from the Imran76Awan/Daily-Tasks repository on GitHub — no sign-in required.

Get-CloudSyncHealthReport.ps1 View full repo →

Required Graph permission is Synchronization.Read.All — a read-only scope, deliberately, since this script only reports on job health and should never be able to trigger, restart, or modify a sync job itself. Here's the first run against a test tenant, before anyone has looked at the duplicate-attribute conflict from earlier in this post:

PowerShell 7 — Get-CloudSyncHealthReport.ps1 (initial run)
Connecting to Microsoft Graph (app-only certificate auth)...
Connected. Tenant: ████████-████-████-████-████████████
Enumerating service principals with active synchronization jobs...
  Service principals scanned            : 212
  Service principals with sync jobs     : 1

================================================================
  ENTRA CLOUD SYNC HEALTH REPORT - 22 Jul 2026 09:02
================================================================
  Synchronization jobs found            : 1

  ServicePrincipal   : Contoso-CloudSync-Config
  JobId              : ████████.████████
  State              : Failed
  LastRunBegan       : 22/07/2026 02:14
  LastRunEnded       : 22/07/2026 02:19
  CountSuccessiveCompleteFailures : 14
  ObjectsFailedThisRun            : 3
  STATUS: FAILING — investigate before this job's next cycle
================================================================
  Healthy jobs   : 0
  Failing jobs   : 1
================================================================

The script's core logic is a straightforward loop: enumerate every service principal, attempt to read its synchronization jobs, and treat an empty result as "not a sync-capable service principal" rather than an error — most of the 212 service principals in a typical tenant have no synchronization configuration at all, and that's expected, not a failure condition.

Get-CloudSyncHealthReport.ps1 — enumeration & classification core
foreach ($sp in $allServicePrincipals) {
    try {
        $jobs = Get-MgServicePrincipalSynchronizationJob -ServicePrincipalId $sp.Id -ErrorAction Stop
    } catch {
        # A permission or throttling error here is a real problem — surface it, don't swallow it
        if ($_.Exception.Message -notmatch 'does not exist|not found') {
            Write-Error "Graph call failed for SP $($sp.Id): $($_.Exception.Message)"
            $script:hadGraphError = $true
        }
        continue
    }
    if (-not $jobs) { continue }  # no sync config on this SP — not an error, just skip

    foreach ($job in $jobs) {
        $last = $job.Status.LastExecution
        $isFailing = ($last.State -ne 'Succeeded') -or ($job.Status.CountSuccessiveCompleteFailures -gt 0)
        # classification and colorized output happen here — omitted for brevity
    }
}

Notice the error handling deliberately distinguishes "this service principal has no sync job" (expected, silently skipped) from "Graph refused this call" (a real problem that gets surfaced and flips a script-level error flag). That distinction is what stops the script from ever reporting a false "0 failing jobs, all healthy" when the true story is "the script couldn't actually talk to Graph." If $hadGraphError is set by the end of the run, the script exits with a non-zero code and an explicit error, rather than printing a clean-looking report that happens to be wrong.

Tip: Don't rely on remembering to run this manually. Schedule it as a Windows Task Scheduler job or an Azure Automation runbook on a daily cadence, and pair it with Microsoft Entra Connect Health, which provides native alerting for sync health issues without you having to build your own notification pipeline. The script in this post is for point-in-time investigation and CSV evidence — Connect Health is for "tell me the moment something breaks."

Proof it worked: re-running after the duplicate attribute is resolved

Back to the renamed employee from the start of this post. The fix, in this case, was entirely on the on-premises side: the duplicate proxyAddresses value was found on a long-disabled leaver account, that stale value was removed from the old account, and the next scheduled Cloud Sync cycle ran without the matching conflict. Here's the same script, run again after that fix and one clean cycle:

PowerShell 7 — Get-CloudSyncHealthReport.ps1 (post-remediation run)
Connecting to Microsoft Graph (app-only certificate auth)...
Connected. Tenant: ████████-████-████-████-████████████
Enumerating service principals with active synchronization jobs...
  Service principals scanned            : 212
  Service principals with sync jobs     : 1

================================================================
  ENTRA CLOUD SYNC HEALTH REPORT - 22 Jul 2026 14:47
================================================================
  Synchronization jobs found            : 1

  ServicePrincipal   : Contoso-CloudSync-Config
  JobId              : ████████.████████
  State              : Succeeded
  LastRunBegan       : 22/07/2026 14:40
  LastRunEnded       : 22/07/2026 14:44
  CountSuccessiveCompleteFailures : 0
  ObjectsFailedThisRun            : 0
  STATUS: HEALTHY
================================================================
  Healthy jobs   : 1
  Failing jobs   : 0
================================================================

CountSuccessiveCompleteFailures back at 0 and the job state showing Succeeded is a good sign, but per the gotcha earlier in this post, it is not the final proof. The actual proof is checking the specific object that was failing — the renamed employee's user record — directly:

PowerShell 7 — confirming the specific object synced correctly
Get-MgUser -UserId "a.h████@contoso.com" -Property displayName,surname,mail | Format-List

DisplayName : Amelia H████ (updated surname)
Surname     : H████ (updated surname)
Mail        : a.h████@contoso.com

That's the full loop closed: the job-level counter reset, and the specific object that was silently stuck for three weeks now reflects the correct attribute. Re-check both every time — the job counter tells you the engine is running cleanly; checking the actual object tells you the data problem that started this whole investigation is genuinely gone.

References

Have you actually checked whether your Cloud Sync job's failure counter has been quietly climbing, or are you assuming it's healthy because nobody has complained yet? Run the script, check the counter against the portal, and see what turns up — I'd like to know how many tenants are sitting on a growing failure count nobody has noticed.

Share this post
LinkedIn X / Twitter Reddit Bluesky

More from EndpointWeekly

Entra ID
Entra Hybrid Join Stuck Pending? Here's How to Fix It
A device stuck in a pending hybrid join state blocks Conditional Access silently. Here's how to diagnose and…
Entra ID
The Primary Refresh Token (PRT): How Entra ID SSO Actually Works…
The PRT is the token that powers silent SSO across every Microsoft 365 app on your…
Entra ID
Azure Files Kerberos Auth With Hybrid Identities
Storage account keys give everyone root-level access with no per-user permissions. Here's the hybrid identity fix…