Tutorial · powershell · 09 May 2026

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.