Private/Invoke-SPMWithRetry.ps1

function Invoke-SPMWithRetry {
    <#
    .SYNOPSIS
        Executes a script block and retries on SharePoint/Graph throttling (HTTP 429/503), honoring Retry-After.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [scriptblock]$ScriptBlock,

        [int]$MaxRetries = 6
    )

    $attempt = 0
    while ($true) {
        try {
            return & $ScriptBlock
        }
        catch {
            $attempt++
            $status = $null
            $retryAfter = $null

            $response = $_.Exception.Response
            if ($response) {
                try { $status = [int]$response.StatusCode } catch { $status = $null }
                try {
                    $values = @($response.Headers.GetValues('Retry-After'))
                    if ($values.Count -gt 0) { $retryAfter = $values[0] }
                }
                catch { $retryAfter = $null }
            }
            if (-not $status -and $_.Exception.Message -match '(?i)429|too many requests|throttl|503|server too busy') {
                $status = 429
            }

            if (($status -in 429, 503) -and $attempt -le $MaxRetries) {
                $delay = [Math]::Min(10 * $attempt, 120)
                $parsed = 0
                if ($retryAfter -and [int]::TryParse([string]$retryAfter, [ref]$parsed) -and $parsed -gt 0) {
                    $delay = $parsed
                }
                Write-Warning "Throttled (HTTP $status) - attempt $attempt/$MaxRetries, waiting $delay seconds..."
                Start-Sleep -Seconds $delay
                continue
            }

            throw
        }
    }
}