Writing PowerShell that survives Graph throttling
Microsoft Graph will throttle you. A 429 is not an error to log and move past — it is an instruction, and it tells you exactly how long to wait.
A script that works against a 200-user tenant and falls over against a 20,000-user tenant usually has one bug: it treats HTTP 429 as a failure instead of as a schedule.
429 is a conversation
When Graph returns 429 Too Many Requests, it also returns a Retry-After header. That header is not advisory. It is the service telling you, precisely, when it will accept the next call. Sleep for that long and the next request succeeds. Sleep for a hard-coded two seconds and you are guessing.
The pattern
Wrap the call. On a 429, read Retry-After, wait, retry. Cap the retries so a genuinely broken request doesn’t loop forever.
function Invoke-GraphWithBackoff {
param([scriptblock] $Call, [int] $MaxRetries = 5)
for ($attempt = 0; $attempt -le $MaxRetries; $attempt++) {
try {
return & $Call
}
catch {
if ($_.Exception.Response.StatusCode -ne 429 -or $attempt -eq $MaxRetries) {
throw
}
$wait = [int] $_.Exception.Response.Headers['Retry-After']
Start-Sleep -Seconds ([Math]::Max($wait, 1))
}
}
}
Don’t earn the 429 in the first place
Backoff handles the throttling you cannot avoid. The throttling you can avoid comes from asking for too much: request only the properties you need with $select, page in reasonable sizes, and batch related reads. A polite client gets throttled less.