Snippet

Retry a rate-limited planning operation

A complete PowerShell script that retries a synthetic operation when it returns retry-after guidance.

powershellOct 6, 2026MIT

AI-generated, human-reviewed: the first draft was generated by an AI agent; I fact-checked, tested, and edited it. AI use policy

What it does#

Wraps a planning operation with retry handling. The sample operation is synthetic and local, but the control flow mirrors what you need around Azure Cost Management calls: honor retry guidance, back off, and fail loudly after the retry budget is spent.

You need#

  • PowerShell 7.6.2 or newer
  • No Azure modules for this local sample
  • For a real Azure collector, Az.Accounts 5.5.0 or newer and permission to read Azure Cost Management data for the target subscriptions

Where it goes#

Save it as Invoke-PlanningOperationWithRetry.ps1. Use the function around the one line that performs your real API call.

Invoke-PlanningOperationWithRetry.ps1
# SPDX-License-Identifier: MIT
# Copyright (c) 2026 Sarang Datye
#Requires -Version 7.4
[CmdletBinding()]
param()

function Invoke-PlanningOperationWithRetry {
    param(
        [Parameter(Mandatory)]
        [scriptblock]$Operation,

        [ValidateRange(0, 20)]
        [int]$MaxRetries = 5,

        [ValidateRange(0, 60)]
        [int]$BaseDelaySeconds = 1
    )

    for ($attempt = 1; $attempt -le ($MaxRetries + 1); $attempt++) {
        $result = & $Operation

        if ($result.Success) {
            return $result.Value
        }

        $isRetryable = $result.StatusCode -in 429, 503
        if (-not $isRetryable -or $attempt -gt $MaxRetries) {
            throw "Planning operation failed with status $($result.StatusCode): $($result.Message)"
        }

        $retryAfterSeconds = 0.0
        $hasRetryAfter = $null -ne $result.RetryAfterSeconds -and [double]::TryParse(
            [string]$result.RetryAfterSeconds,
            [Globalization.NumberStyles]::Float,
            [Globalization.CultureInfo]::InvariantCulture,
            [ref]$retryAfterSeconds
        ) -and -not [double]::IsNaN($retryAfterSeconds)

        $delaySeconds = if ($hasRetryAfter) {
            [Math]::Min(60.0, [Math]::Max(0.0, $retryAfterSeconds))
        } else {
            [Math]::Min(60.0, [Math]::Pow(2, $attempt) * $BaseDelaySeconds)
        }

        Write-Verbose "Attempt $attempt was rate-limited. Waiting $delaySeconds seconds."
        Start-Sleep -Seconds $delaySeconds
    }
}

$script:attempts = 0
$syntheticOperation = {
    $script:attempts++

    if ($script:attempts -lt 3) {
        return [pscustomobject]@{
            Success = $false
            StatusCode = 429
            RetryAfterSeconds = 2 - $script:attempts
            Message = 'Synthetic rate limit'
        }
    }

    return [pscustomobject]@{
        Success = $true
        StatusCode = 200
        Value = [pscustomobject]@{
            Month = '2026-01'
            Cost = 123.45
        }
    }
}

$value = Invoke-PlanningOperationWithRetry -Operation $syntheticOperation -MaxRetries 2 -Verbose
if ($value.Cost -ne 123.45 -or $script:attempts -ne 3) {
    throw 'The retry wrapper did not honor both retry-after responses before succeeding.'
}

$script:attempts = 0
$alwaysRateLimited = {
    $script:attempts++
    [pscustomobject]@{
        Success = $false
        StatusCode = 429
        RetryAfterSeconds = 0
        Message = 'Still rate limited'
    }
}

$threw = $false
try {
    Invoke-PlanningOperationWithRetry -Operation $alwaysRateLimited -MaxRetries 0
} catch {
    $threw = $true
}

if (-not $threw -or $script:attempts -ne 1) {
    throw 'MaxRetries 0 should make exactly one attempt and then fail.'
}

$script:attempts = 0
try {
    Invoke-PlanningOperationWithRetry -Operation $alwaysRateLimited -MaxRetries 1
    throw 'MaxRetries 1 should fail after one retry.'
} catch {
    if ($script:attempts -ne 2) {
        throw 'MaxRetries 1 should make exactly two attempts.'
    }
}

$value | Format-List

Check that it works#

Run:

powershell
pwsh -NoProfile -File ./Invoke-PlanningOperationWithRetry.ps1

You should see a synthetic monthly cost after two retries. In a real collector, replace the synthetic operation body with your Azure Cost Management request.