Repository navigation
User Import Troubleshooting
Troubleshooting → User Import Troubleshooting
This page is for administrators whose user and licence data is out of date or wrong: new users or user changes that don't arrive, leavers who still count as enabled, renamed users, and people counted as licensed or unlicensed when they shouldn't be. For each problem it gives the log lines to look for and what to do.
Affects: builds without the fix for #664, which is every stable build up to and including 1843, when the user import's checkpoint was kept in Azure Cache for Redis. Without Redis, those builds held the token in memory, so every user import read the full user list and the token could never get old. The fix shipped in Stable build 1846, which keeps the checkpoint in the solution's AnalyticsState Azure table instead.
Symptoms
- New Entra users are not imported. A user can still appear as a basic row created from their activity, but with no department, job title, office, manager or Entra object ID.
- Changes to existing users stop arriving: department, job title, office, manager, company, country, state, postal code, usage location and mail. Every breakdown by department or organisation freezes on the day the token stopped working.
- Nothing looks wrong: every run still records a
FinishedSectionImportevent for User metadata refresh.
Log signature. Every user import writes this error (the token, date and IDs vary):
Got HTTP exception calling https://graph.microsoft.com:443/v1.0/users/delta?$select=...&$expand=manager&$deltatoken=<token>: ... 400 ... Response body: {"error":{"code":"Request_UnsupportedQuery","message":"DeltaLink older than 30 days is not supported.", ...}}
followed by User import - read 0 updated users from Graph API, using last delta. That second line on its own is normal: it also appears when nothing changed in the tenant between two runs. What identifies this problem is the same /users/delta error on every run. To check, run this query against the deployment's Application Insights:
traces
| where timestamp > ago(30d)
| where severityLevel == 3
| where message contains "/users/delta" and message contains "Request_UnsupportedQuery"
| summarize runs = count(), first_seen = min(timestamp), last_seen = max(timestamp)On a build with the fix, expect at most one hit each time a token expires, immediately followed by the recovery warning shown below. A hit on every run means the build does not have the fix.
Cause. The user import keeps a Microsoft Graph delta token, so each run reads only what changed since the last one. Graph stops accepting a token once it is too old. In practice /users/delta answers 400 with error code Request_UnsupportedQuery; the documented forms are 410 Gone and the error code syncStateNotFound. A token only gets that old when the user import has not completed for weeks: the import was switched off (GraphUsersMetadata), the web-job was stopped, or runs kept failing. Builds without the fix never discarded a refused token, and the stored token had no expiry, so the stall carried on indefinitely — even after whatever stopped the imports had been fixed.
Fix: upgrade. Builds with the fix recover without any admin action, in the same run: they discard the refused token and read the full user list again, which also catches up on everything that was missed. Expect this sequence:
User import - Microsoft Graph rejected the stored /users/delta token (HTTP 400 (BadRequest), Graph error code 'Request_UnsupportedQuery'): it has expired, or Graph has reset synchronisation. Discarding the token and reading the full user list again in this run, which also catches up on every change made while it was unusable.
Cleared persisted user delta token.
User import - read 12,345 users (all) from Graph API
That run reads every user, like a first install, so on a large tenant it takes longer than a normal run.
On builds with the fix you can also force a full read yourself, with Administration → User import → Clear checkpoint. The page also shows whether a checkpoint is stored and when the user import last completed. See Web portal → User import.
Any other failure — 401, 403, 429, 5xx or a timeout — keeps the token, because it says nothing about the token's age. Instead the run is reported as not finished: it logs User import - the /users/delta read stopped on page ..., then User import - NOT moving the user checkpoint forward: ... and User metadata refresh did not complete successfully; it will be retried on the next cycle., records no FinishedSectionImport event, and is tried again on the next cycle.
On a build without the fix, upgrade. Until you can, delete the stored token: the next run then reads the full user list, exactly as a first install does, and catches up. Those builds kept it in Azure Cache for Redis, database 0, under the key UserDeltaCode-<tenant-id>-v2 (UserDeltaCode-<tenant-id> on Stable 1830 and earlier), where <tenant-id> is the Entra tenant GUID in lower case with hyphens. The first user import after upgrading from such a build reads every user once anyway, because the checkpoint moves to Azure Table storage and its old value isn't carried over.
Fixed in Stable 1856 (PR #768).
Affects: every stable build up to and including 1854.
Symptom. People who have left the organisation still count as enabled users (dbo.users.account_enabled = 1) long after their Microsoft Entra account was disabled or deleted. Copilot Adoption then can't suggest reclaiming their licences, and every figure that counts enabled accounts includes them.
Cause. Microsoft Graph's /users/delta reports each change once. Builds before 1856 threw away the entries for disabled and deleted accounts before saving anything, and the stored checkpoint then moved past them, so those rows were never updated again.
What the importer does from 1856
- An account Graph reports as disabled is still not added if it's new, but an existing row for it is updated like anyone else's, with
account_enabled = 0. - An account Graph reports as deleted has its rows marked
account_enabled = 0. Nothing else in the row changes: department, manager and the rest are kept, and the row isn't deleted. - After a complete read of the directory (a read with no checkpoint, from the first page to the end), any enabled row whose Entra object ID the read didn't list is also marked disabled, because a complete read never reports deletions; it simply leaves deleted accounts out.
- Only rows that carry an Entra object ID are changed. Rows that other imports created from a sign-in name alone, with no object ID, are never touched.
-
account_enabled = 0is written whether or not the person is insideUserGroupsFilter. Deleting an account removes it from every group, so a leaver is usually outside the filter by the time the import hears about it. Marking their existing row disabled stores nothing new about them.
The import logs each kind separately:
User import - the read also reported 12 disabled and 3 deleted account(s). They are not added to the users table; any row it already has for them is marked disabled.
User import - marked 14 users row(s) disabled for the 12 disabled and 3 deleted account(s) Graph reported.
Expect a one-off drop in enabled users. The first complete read after upgrading catches up every account deleted since the database started storing Entra object IDs, which on an older installation can be a large number. The log line says how many:
User import - marked 1,234 users row(s) disabled whose account is no longer in Entra ID: this complete read listed 20,000 users, in line with Graph's count of 20,010, and not them, so they have been deleted. The first complete read after upgrading to this version also catches up every account deleted before it, so expect a one-off drop in the number of enabled users of about this many.
An ordinary run reads only what changed, so it doesn't do this catch-up. It happens at the next complete read: for example after you change UserGroupsFilter, or when you choose Administration → User import → Clear checkpoint (see Web portal → User import). Clear the checkpoint if you want the catch-up done at a time you pick, for example before a licence review.
A drop in the total number of users, rather than in enabled users, has a different cause: see Placeholder users from concealed usage reports are deleted after upgrading.
The safety check. A complete read that silently missed part of the directory would otherwise disable everyone it missed, and nothing would turn them back on. So before marking anyone disabled for being absent, the import asks Graph how many users the directory has (GET /users/$count, with the header ConsistencyLevel: eventual; it needs only the User.Read.All permission the user import already has). The number of users the read listed must be within 1% of that count, or within 10 users if that is more, which allows for the count lagging slightly behind recent changes. If the count can't be read, the import falls back to a check against the users table: it goes ahead only if the read listed at least as many of the table's accounts as it left out. When a check fails, nobody is disabled for being absent, and the import logs a warning instead:
Warning (after User import - NOT marking <N> account(s) disabled although this complete read of the directory did not list them:) |
What it means | What to do |
|---|---|---|
it listed <N> users, but Graph counts <M> in the directory, so it may have missed some. Graph's count can lag behind a large deletion for a while |
The read came up short. Straight after a large clean-up of the directory, Graph's count can still include the deleted accounts for a while. | Nothing straight away. If it repeats, check the user import's other warnings for a read that stopped early. |
it listed <N> users, but Graph counts only <M> in the directory - too far apart for either to be relied on |
The read listed more users than Graph counts. | As above. |
Graph's count of the directory's users could not be read to confirm the read was complete, and the read left out more of the users table's accounts (<N>) than it listed (<M>), as a read that lost part of the directory would |
$count failed, and the fallback check wasn't met. On a tenant with many old leavers this can keep happening until $count can be read. |
Look for the preceding User import - couldn't read Graph's count of the directory's users: ... line, and fix what it reports. |
it listed no users at all |
The read was empty. | Check the user import's errors. |
Each warning ends: They are checked again on the next complete read (clearing the user checkpoint on the User import page forces one). Accounts Graph reports as disabled or deleted are still marked as usual. So the check only holds back the catch-up of accounts that are missing from the read; accounts Graph reports as disabled or deleted are always marked.
Renamed users — since Stable 1856 (PR #768). Builds before 1856 kept the old row and added a new one when a sign-in name changed, splitting the person's history between SQL user IDs. From 1856 the importer instead renames the existing row in place: the row carrying the person's Entra object ID, or the newest of them if several do, provided the new sign-in name isn't on another person's row. Its SQL user ID, activity history and incoming manager links stay attached to the same person. No extra permission or schema migration is required.
Existing historical copies are not merged or deleted. When a row already has the current name and the same object ID, it remains the current row and the older copies remain intact. When a user can't be matched to a row safely, for example because their new sign-in name is on another person's row, or Graph listed two sign-in names for them, the importer quarantines that user and logs a warning:
User import - identity conflicts: <N> user(s) quarantined (destinationOtherObjectId=<N>, conflictingGraphNames=<N>, upnTooLong=<N>, changedDuringRename=<N>). Nothing that depends on their identity is written for them; everyone else is imported, and they are re-checked every cycle. First <N>: ...
The line identifies the first few users by object ID, never by sign-in name. Everyone else is imported as usual and the user checkpoint moves on. The quarantined users' latest Graph profiles are kept in the UserIdentityQuarantine partition of the AnalyticsState table (see Runtime state reference) and checked again every cycle, so a conflict that is resolved in Entra ID clears by itself. The checkpoint is held back only when the quarantine can't be stored: when that partition can't be read or saved, or more users are quarantined than it keeps. The importer then logs an error ending NOT moving the user checkpoint forward, so none of their changes are lost; they are read again next cycle. It never silently picks a row or overwrites another person's identity. If a conflict persists, use Administration > User data lookup and your DBA to compare the conflicting SQL identities with Graph, take a backup, and resolve it without deleting history. Clearing the checkpoint does not resolve a conflict.
Symptoms. Copilot Adoption or Licence Activity lists someone as unlicensed who holds the licence in the Microsoft 365 admin center, or the other way round.
Several causes look alike, and one “last updated” date cannot distinguish them. Open Administration → User data lookup (or Troubleshoot this person from a report row) and work through User Licence Diagnostics. Both Administration and See PII are required. Compare Graph read-only before requesting the bounded one-person refresh; do not clear the whole tenant checkpoint as the default remedy.
-
Compare with Microsoft Graph. This reads the person's licences from the directory now.
- Only in directory: the database is behind. Check How fresh are the stored licences? If the user import is off or overdue, fix that first (see the Health page). Otherwise use Refresh this person from the directory and watch it finish.
- Only in database: the licence was removed in Entra after the last licence refresh. A refresh corrects it.
- Match: the stored data is right, so the cause is how the report reads it. Go to step 2.
- Cannot tell: the directory's answer was incomplete (for example a missing permission), or several SKUs share one product name. Nothing will be removed on an incomplete answer.
-
Why does a report count this person this way? This re-runs the report's own rule for this one person.
- A rolling period uses the licences stored now: someone whose licence was removed yesterday counts as unlicensed for the whole period.
- A past period uses licence history, which only knows when a refresh observed a change, and nothing from before history tracking began.
- A what-if seat selection on the report changes which licences count.
- Copilot seat rules and catalogue. If the person's Copilot comes through a SKU the rules don't count, it's listed as Rules and catalogue disagree. The rules are not changed from the portal. Test the effect with a what-if seat selection on Copilot Adoption, and report the SKU so a later build can classify it.
- Import scope. Someone outside the user scope is not stored, so no report can count them. While the scope can't be resolved, live reads and refreshes are unavailable rather than guessed.
After a refresh, the reports show the corrected licence without waiting for their caches. A refresh that found nothing to change still records the person as confirmed, which the next look at the page shows as Last confirmed for this person.
Not a fix for the whole tenant. If many people are wrong, the cause is the user import, not individual records: check the Health page and the expired user delta token section above, and use Administration > User import to read every user again.
Optional-feature symptoms (Copilot Adoption settings, the report filter, licence activity, billing, prompt categories, the leadership group and the Agent 365 catalogue) have moved to Web Portal Troubleshooting → Optional-feature symptoms. Deployment failures are covered under Troubleshooting → Network restrictions.
Return to Troubleshooting · All topics in this guide
Previous: Teams and Calls Troubleshooting · Next: Usage Reports and Email Troubleshooting
- Copilot analytics overview
- Copilot Adoption user guide
- Copilot Adoption setup (admins)
- Work Trend Index
- Copilot investment
- Copilot spend
- Agent costs
- Prompt categories
- Copilot credit history (preview)
- Agent 365 package catalog
- The Copilot data in depth
- Prerequisites
- App registrations setup
- Install with the installer
- Verify the deployment
- Security & permissions model
- Private endpoints (optional)
- Certificate authentication (optional)
- Manual installation
- Activity & usage analytics (Power BI)
- SharePoint page tracking