Repository navigation
Performance challenges with large tenants #1236
Description
Creating this issue as initial placeholder to address several known challenges with very large tenants.
The issues are sourced from real world large tenant experiences. This work item must not be addressed directly, but rather used as base for creating actual work items to address problematic areas.
The analysis showed several areas worth applying new approaches for:
$skiptoken expiration
Microsoft Graph documentation explicitly makes a reference to dedicated error when paging through vast amount of data: https://learn.microsoft.com/en-us/graph/paging?tabs=http#error-handling
The DirectoryPageTokenNotFoundException must be accounted for, handled and the whole process of grabbing data restarted.
Additionally, MS Graph can, without specifying any concrete reason, expire $skipToken - especially if there was too long time between calling the individual pages.
Any retries that happen, must account for the $skipToken change, and as per documentation, use the value from last successful response, instead of the original one, if that failed.
retry back-off
The analysis shows an exponential retry back off of 3s, 6s, 12s, 24s, 48s - come from the default start setting of 3 seconds x 5 retries.
We must keep track of whether this is a repeatable pattern with large tenants, or an isolated case. If repeatable, it is worth considering making the default larger - e.g. 5 or 6 seconds. Because if there are some real intermittent issues, better to wait 2 seconds more for the first retry, then risking entering throttling loop because of too short base wait.
Keeping this for deeper analysis.
Here is a comprehensive analysis of one particular failure:
Support Package Analysis
Time Range: 2026-05-12 23:12:24 → 2026-05-22 00:24:49 (9 days, 1 hour, 12 minutes)
Environment: PS 7.5.5 on Microsoft Windows 10.0.26200
Module Version: ZeroTrustAssessment 2.2.50
Summary
- Total log messages: 1,831
- Errors: 2 | Warnings: 37 | Critical: 0
| Error Category | Count |
|---|---|
| DNS Resolution Failure | 13 |
| HTTP Timeout (300s) | 10 |
| Stream Copy Error | 5 |
| Authentication Failure | 4 |
| Null Reference | 2 |
| Sign-In Log Size Limit | 1 |
| Skip Token Expiration | 1 |
| Non-Retryable (400) | 1 |
| Other | 2 |
Issues Found
Issue 1: Authentication Failures (Initial)
- Severity: Error + Warning (Issues 1–4)
- Time: Session start
- Function:
Connect-ZtAssessment - Source: Connect-ZtAssessment.ps1
- Message:
InteractiveBrowserCredential authentication failed: User canceled authentication - Root Cause: The user closed the browser window on the first authentication attempt. They subsequently retried and connected successfully (the assessment proceeded).
- Impact: None — transient user action, assessment continued.
Issue 2: Sign-In Log Size Limit Hit
- Severity: Warning (Issue 5)
- Time: During export phase
- Function:
Export-ZtGraphEntity - Source: Export-ZtGraphEntity.ps1 (~line 226)
- Message:
Sign-in log export reached size limit of 1024 MB (current: 1025.71 MB) - Root Cause: Large tenant with a very high volume of sign-in logs. The 1 GB cap was reached.
- Impact: Low — sign-in log export was truncated but assessment continues. This is expected behavior.
- Fix: If more sign-in data is needed, increase
ZeroTrustAssessment.Export.SignInLog.MaxSizeBytes.
Issue 3: Skip Token Expiration — ServicePrincipalSignIn Export Failed
- Severity: Warning (Issues 9–12)
- Time: ~52 minutes into export
- Function:
Invoke-ZtRetry→Export-ZtGraphEntity→Invoke-ZtTenantDataExport - Source: Export-ZtGraphEntity.ps1 (~line 190), Invoke-ZtRetry.ps1
- Message:
Skip token is null. It may have a typo or it has expired.(HTTP 400 on page 53) - Root Cause: Graph API skip tokens are time-bound. Repeated stream errors and timeouts during pagination slowed the export enough for the token to expire server-side. The 400 was correctly marked as non-retryable by
Test-ZtRetryableError— retrying the same expired token would fail again. - Impact: ServicePrincipalSignIn export failed (52m57s wasted).
Issue 4: ServicePrincipal Null Reference
- Severity: Warning (Issues 14–15)
- Time: After ~1h25m of export
- Function:
Invoke-ZtTenantDataExport→Wait-ZtTenantDataExport - Source: Invoke-ZtTenantDataExport.ps1, Wait-ZtTenantDataExport.ps1
- Message:
You cannot call a method on a null-valued expression - Root Cause: Likely a cascading failure — a variable was null due to a prior failed network operation. The sustained network instability (stream errors + DNS failures) caused an upstream operation to return null, and the ServicePrincipal export called a method on it.
- Impact: ServicePrincipal export failed (1h25m42s wasted).
Issue 5: Sustained HTTP Timeouts During Application Export
- Severity: Warning (Issues 16–26)
- Time: ~03:00–04:00 UTC on May 13
- Function:
Invoke-ZtRetry - Source: Invoke-ZtRetry.ps1
- Message:
The request was canceled due to the configured HttpClient.Timeout of 300 seconds elapsing - Root Cause: Graph API responses for Application pages (520s–530s of 999-item pages) consistently exceeded the 300s timeout. This is a very large tenant. Each timeout added 5+ minutes of delay, compounding the pagination problem.
- Impact: Contributed to the Application export eventually failing via DNS failure.
Issue 6: DNS Resolution Failure — Application Export Failed
- Severity: Warning (Issues 27–34)
- Time: 04:12–04:23 UTC on May 13
- Function:
Invoke-ZtRetry→Export-ZtGraphEntity→Wait-ZtTenantDataExport - Source: Invoke-ZtRetry.ps1, Export-ZtGraphEntity.ps1
- Message:
The requested name is valid, but no data of the requested type was found. (graph.microsoft.com:443)— 5 consecutive retries exhausted - Root Cause: DNS could not resolve
graph.microsoft.com. This was a sustained outage lasting ~11 minutes. All 5 retry attempts (with exponential backoff: 3s, 6s, 12s, 24s, 48s) failed. - Impact: Application export failed on page 532 after 5h7m of export time. All progress lost.
Issue 7: DNS Failure During Test Phase
- Severity: Warning (Issues 35–39)
- Time: 04:32 UTC on May 13 (during Stage 2: Running Tests)
- Function:
Invoke-ZtRetry(called fromInvoke-ZtGraphRequestCachefor/v1.0/organization) - Source: Invoke-ZtRetry.ps1
- Message: Same DNS resolution failure, 5 consecutive attempts failed
- Root Cause: DNS outage persisted into the test phase. The assessment continued running tests but any test needing live Graph calls would have failed.
- Impact: Tests dependent on live Graph API calls during this window likely produced incorrect results.
Failed Exports
| Export | Duration | Error | Recommendation |
|---|---|---|---|
| Application | 5h 7m | DNS resolution failure on page 532 | Re-run during stable network window; consider off-peak hours |
| ServicePrincipal | 1h 25m | Null-valued expression (cascading failure) | Re-run; investigate if network instability caused upstream null |
| ServicePrincipalSignIn | 52m 57s | Skip token expired (400 Bad Request) on page 53 | Re-run with stable network; stream errors slowed pagination causing token expiry |
Successful Exports (13 of 16)
Device, SignIn, ConfigurationPolicy, RoleAssignmentScheduleInstance, RoleAssignmentScheduleInstanceGroup, RoleEligibilityScheduleInstance, RoleManagementPolicyAssignment, RoleDefinition, UserRegistrationDetails, RoleAssignment, User, RoleEligibilityScheduleInstanceGroup, RoleAssignmentGroup
Root Cause Analysis
The dominant issue is network instability on the customer's machine. Three distinct network failure modes were observed during the assessment:
- Stream copy errors (5 occurrences) — connections dropped mid-transfer, all recovered on retry
- HTTP timeouts (10 occurrences) — 300s timeout exceeded, indicating either slow Graph API responses for this large tenant or network latency
- DNS resolution failures (13 occurrences) — sustained inability to resolve
graph.microsoft.com, lasting ~11+ minutes, which was the fatal blow that killed the Application export after 5 hours of work
The cascade was: stream errors + timeouts slowed pagination → skip token expired for ServicePrincipalSignIn → DNS outage killed Application and affected the test phase. The ServicePrincipal null-reference failure is likely also network-related.
This is a very large tenant (~530K+ applications, 1 GB+ of sign-in logs), which makes exports slow and amplifies the impact of any network disruption.
Recommendations
- Re-run the assessment during a stable network window — the DNS outage at ~04:12 UTC was the primary cause of failure. Ensure reliable DNS resolution and network connectivity before starting.
- Check DNS and proxy configuration — verify the customer's DNS can reliably resolve
graph.microsoft.com. Check for any corporate proxy, VPN, or firewall that might cause intermittent DNS failures. - Run during off-peak hours — the HTTP timeouts on Application pages suggest Graph API is responding slowly for this tenant. Off-peak hours may yield faster responses.
- Consider wired network — if running on WiFi, switch to wired to reduce stream copy errors.
- The initial auth failures are benign — the user simply cancelled the browser prompt once and retried successfully.
- Sign-in log 1 GB limit is expected — if the full sign-in dataset is needed, set
ZeroTrustAssessment.Export.SignInLog.MaxSizeBytesto a higher value, but note this will increase export time significantly.
Activity
jamesmor commentedon May 27, 2026
In my tenant (~1.7M directory objects), I’m encountering both the $skipToken issue described above and a consistent memory exhaustion.
Observations
Environment
Questions
Happy to provide logs or run targeted tests if that helps isolate the issue.
jamesmor commentedon May 27, 2026
An update: the latest run with 128GB of RAM completed all but the RoleAssignmentGroup and RoleAssignmentScheduleInstanceGroup exports.
kodevza commentedon May 27, 2026
James Morris (@jamesmor) Would you be kind enough to test one more version?
I prepared a change that should address the memory retention issue during Graph export.
Before the fix, the export completed successfully, but retained too much managed memory after GC:
GC memory after GC: ~4.54 GB
WorkingSet peak: ~8.54 GB
Private peak: ~8.65 GB
Elapsed: ~4m 15s
After the fix, the same benchmark passed with much lower retained memory:
GC memory after GC: ~124 MB
WorkingSet peak: ~716 MB
Private peak: ~791 MB
Elapsed: ~4m 14s
The relevant commit is here:
kodevza@146cf7c
The change makes Export-ZtGraphEntity capture the Graph @odata.nextLink before writing each page, then clears the exported page’s heavy value payload in a finally block. This prevents retained response wrapper objects from keeping all exported entities alive in memory.
I also added a regression test to verify that exported page values are released after writing and released:
https://github.com/kodevza/zerotrustassessment/releases/tag/v2.4.0-memory_gc_test
The README contains a short 5-line installation guide.
jamesmor commentedon May 27, 2026
Thanks, I've got 2.40.0 running now. In most of my tests earlier today, I was able to skate by with 127GB/128GB and finish 14 of 16 tests, but then got hung on the RoleAssignmentGroup and RoleAssignmentScheduleInstanceGroup exports. The whole system has been hung, so I haven't been to collect PSFPackages after I crash the box and bring it back up.
jamesmor commentedon May 28, 2026
Version 2.4.0 started out using considerably less memory than what I had previously seen, but it has crept up over the last couple hours to near 113GB in use. As seen in the image above, the RoleAssignmentGroup and RoleAssignmentScheduleInstanceGroup exports are still not completing.
kodevza commentedon May 28, 2026
James Morris (@jamesmor) Thx for logs and check but I think we mixed two topics in this thread a bit.
This issue was originally more of a large-tenant / skipToken / retry placeholder. Your memory exhaustion case looks specific enough to deserve a separate issue. Could you open a new issue for the memory behavior and link this thread there? BTW: I will provide new version soon.
Anton Staykov (@astaykov) Is this retry issue currently blocking you or users in large tenants?
If yes: would you accept using the version from my fork for now with the fix, or do you specifically want this fixed upstream in the Microsoft repo?
jamesmor commentedon May 28, 2026
I'll open the new Issues shortly; one for the memory exhaustion and one for the RoleAssignmentGroup and RoleAssignmentScheduleInstancegroup exports not completing and blocking progress on anything that relies on them. Thank you! I really appreciate the assistance.
leogasparini commentedon Jul 14, 2026
Hi, I also have similar issues as James Morris (@jamesmor) with a huge tenant and was testing some potential fixes if it could be useful:
leogasparini#1