Skip to content

A GitHub secondary rate limit pauses polling for 10 minutes instead of the ~60 s Retry-After, and an Azure DevOps 429's Retry-After is ignored #385

Open

Description

@matt-edmondson

What's wrong

GitHub. IsRateLimitResponse (BuildMonitor/Providers/GitHub.cs ~L873) correctly classifies a secondary (abuse) rate limit through its "rate limit" message, even when X-RateLimit-Remaining is far from 0. ParseRateLimitResetTime (~L857) then reads only X-RateLimit-Reset. On a secondary limit that header is the reset of the primary hourly window, often 30–60 minutes away, and is unrelated to how long GitHub wants the client to back off. The Retry-After header that GitHub sends with secondary limits (typically 60 s) is never read. GetRateLimitWaitTime() then waits min(reset − now, 10 min).

Azure DevOps. The 429 handler (BuildMonitor/Providers/AzureDevOps.cs ~L460) calls OnRateLimitExceeded() with no reset time, so the Retry-After that ADO sends is ignored too. The next request only gets the generic backoff, which can be well short of what the server asked for, so the client keeps getting throttled.

Failure scenario

The review that found this verified the GitHub case. A 403 SecondaryRateLimitExceededException with these headers:

  • X-RateLimit-Remaining: 4000
  • X-RateLimit-Reset: now + 45 min
  • Retry-After: 60

leaves the provider at Status = RateLimited with GetRateLimitWaitTime() == 00:10:00. Every GitHub request then waits 10 minutes rather than about 60 s, even though the hourly budget is nearly full. While the update pass is stuck (see the companion issue on the shared update loop), every other provider stalls with it.

Suggested fix

  • GitHub: when Retry-After is present, use now + Retry-After as the reset time. Use X-RateLimit-Reset only when X-RateLimit-Remaining == 0. Otherwise, for a secondary limit with no Retry-After, back off about 60 s, as GitHub's guidance says.
  • Azure DevOps: read Retry-After from the VssServiceResponseException's response (or the X-RateLimit-Reset/Retry-After headers ADO returns) and pass it to OnRateLimitExceeded(...).

Acceptance criteria

  • A secondary-limit 403 with Retry-After: 60 and a full primary budget yields a wait of about 60 s, not 10 min.
  • A primary-limit 403 (Remaining: 0) still waits until X-RateLimit-Reset.
  • An ADO 429 with Retry-After: N waits about N seconds.
  • Unit tests cover each case through ParseRateLimitResetTime or its replacement.

Related: #341 and #331 cover the pacing and budget model, not how the reset time is derived.

Activity

  1. matt-edmondson commented on Oct 29, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    The GitHub half is in #388. Retry-After is honoured, X-RateLimit-Reset is used only when Remaining: 0, and otherwise the backoff is 60 s. Tests cover each acceptance case.

    The Azure DevOps half is not done, and the suggested fix doesn't work as written. VssServiceResponseException exposes only HttpStatusCode and the message (checked by reflecting over the shipped Microsoft.VisualStudio.Services.WebApi). There is no response object and no headers, so Retry-After can't be read off the exception in the 429 catch.

    What it needs instead:

    • A DelegatingHandler passed to new VssConnection(uri, credentials, handlers) in AzureDevOps.CreateSession. It records the Retry-After of the last 429 into a sink the provider owns.
    • The 429 catch in MakeAzureDevOpsRequestAsync passes that value to OnRateLimitExceeded(...).
    • Before relying on it, confirm whether the client's own VssHttpRetryMessageHandler already retries 429s internally, and how the custom handlers are ordered relative to it.

    That can't be verified here: building a session authenticates against dev.azure.com, and AzureDevOpsSessionTests documents the same limit. The recorder itself can be unit-tested with a fake inner handler, but the wiring can't. Leaving the issue open and unassigned for that half. #388 deliberately says "Part of", so merging it won't close this issue.


    Generated by Claude Code

  2. removed their assignment
    on Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions