Skip to content

feat: Handle rate limit errors by respecting X-RateLimit-Reset and Retry-After headers and returning cached queue length#7684

Merged
JorTurFer merged 9 commits into
kedacore:mainfrom
andrewhibbert:feat_rate_limit_handling
May 3, 2026
Merged

feat: Handle rate limit errors by respecting X-RateLimit-Reset and Retry-After headers and returning cached queue length#7684
JorTurFer merged 9 commits into
kedacore:mainfrom
andrewhibbert:feat_rate_limit_handling

Conversation

@andrewhibbert

@andrewhibbert andrewhibbert commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

Provide a description of what has been changed

Checklist

  • [N/A] When introducing a new scaler, I agree with the scaling governance policy
  • I have verified that my change is according to the deprecations & breaking changes policy
  • Tests have been added (if applicable)
  • Ensure make generate-scalers-schema has been run to update any outdated generated files
  • Changelog has been updated and is aligned with our changelog requirements, only when the change impacts end users
  • [N/A] A PR is opened to update our Helm chart (repo) (if applicable, ie. when deployment manifests are modified)
  • A PR is opened to update the documentation on (repo) (if applicable)
  • Commits are signed with Developer Certificate of Origin (DCO - learn more)

Fixes #7683

Relates to: kedacore/keda-docs#1749

Follow on from #7192 and comment #7192 (review)

…try-After headers and returning cached queue length

Signed-off-by: andrewhibbert <[email protected]>
@andrewhibbert andrewhibbert requested a review from a team as a code owner April 24, 2026 14:47
@snyk-io

snyk-io Bot commented Apr 24, 2026

Copy link
Copy Markdown

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions

Copy link
Copy Markdown

Thank you for your contribution! 🙏

Please understand that we will do our best to review your PR and give you feedback as soon as possible, but please bear with us if it takes a little longer as expected.

While you are waiting, make sure to:

  • Add an entry in our changelog in alphabetical order and link related issue
  • Update the documentation, if needed
  • Add unit & e2e tests for your changes
  • GitHub checks are passing
  • Is the DCO check failing? Here is how you can fix DCO issues

Once the initial tests are successful, a KEDA member will ensure that the e2e tests are run. Once the e2e tests have been successfully completed, the PR may be merged at a later date. Please be patient.

Learn more about our contribution guide.

@keda-automation keda-automation requested a review from a team April 24, 2026 14:47
@andrewhibbert andrewhibbert changed the title Feat rate limit handling feat: Handle rate limit errors by respecting X-RateLimit-Reset and Retry-After headers and returning cached queue length Apr 24, 2026
Comment thread pkg/scalers/github_runner_scaler.go Outdated
Comment thread pkg/scalers/github_runner_scaler.go Outdated
…try-After headers and returning cached queue length

Signed-off-by: andrewhibbert <[email protected]>
@keda-automation keda-automation requested a review from a team April 24, 2026 17:03
Comment thread pkg/scalers/github_runner_scaler.go Outdated
Comment thread pkg/scalers/github_runner_scaler.go Outdated
…try-After headers and returning cached queue length

Signed-off-by: andrewhibbert <[email protected]>
@keda-automation keda-automation requested a review from a team April 24, 2026 18:12
@rickbrouwer

rickbrouwer commented Apr 25, 2026

Copy link
Copy Markdown
Member

/run-e2e github*
Update: You can check the progress here

Comment thread pkg/scalers/github_runner_scaler.go Outdated
Comment on lines +729 to +733
s.logger.V(1).Info(fmt.Sprintf(
"Github API rate limit exceeded. Cached queue length: %d, last checked at %s",
s.previousQueueLength,
s.previousQueueLengthTime,
))

@rickbrouwer rickbrouwer Apr 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I was wondering about, when we fall back to the cached queue length, the only signal to the operator is a V(1) log line. Could we also create an event? That way tools like Argo CD would pick it up and show it on the resource directly, which feels like a much better fit here. Not every user has access to the KEDA operator logs (or knows to look there), and I think this is actually an important signal. Without it, unexpected scaling behaviour can be hard to explain. Wdyt?

(As a smaller secondary thing, it might
be worth bumping the V(1) log to V(0) so there's continuous visibility in the logs while we're rate-limited, not just at the transition. But the event is the main thing.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could the log be spammy? IDK how often this happens so maybe it's too much? (I have 0 idea, just asking) the event is 100% worth it ❤️

Comment thread pkg/scalers/github_runner_scaler.go Outdated
func (s *githubRunnerScaler) getRateLimit(header http.Header) (RateLimit, error) {
var retryAfterTime time.Time

remainingStr := header.Get("X-RateLimit-Remaining")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this header always set? Do we need to check if it's present before trying to cast it?

Comment thread pkg/scalers/github_runner_scaler.go Outdated
return RateLimit{}, fmt.Errorf("failed to parse X-RateLimit-Remaining header: %w", err)
}

resetStr := header.Get("X-RateLimit-Reset")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this header always set? Do we need to check if it's present before trying to cast it?

Comment thread pkg/scalers/github_runner_scaler.go Outdated
Comment on lines +729 to +733
s.logger.V(1).Info(fmt.Sprintf(
"Github API rate limit exceeded. Cached queue length: %d, last checked at %s",
s.previousQueueLength,
s.previousQueueLengthTime,
))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could the log be spammy? IDK how often this happens so maybe it's too much? (I have 0 idea, just asking) the event is 100% worth it ❤️

…try-After headers and returning cached queue length

Signed-off-by: andrewhibbert <[email protected]>
@keda-automation keda-automation requested a review from a team April 27, 2026 11:10
…try-After headers and returning cached queue length

Signed-off-by: andrewhibbert <[email protected]>
@rickbrouwer

Copy link
Copy Markdown
Member

Thanks for adding the event! The event fires on every reconcile while we're rate-limited. That could be a lot of events, because the message includes "last checked at ", so each event is unique, so Kubernetes' event aggregation won't dedupe them. Would it be worth only emitting on state transitions? So once when we go into
the rate-limited state and possibly once when we recover? That would also make the event a more meaningful signal.

Further, does EventTypeWarning fit the cache-hit path? The scaler is working as designed there (degraded but functional), so I was wondering if EventTypeNormal might be a better match. The no-cache path does feel like a real warning though. And on that no-cache path, there we're already returning an error which the reconciler surfaces as an event itself, right? So we might be double-emitting there.

…try-After headers and returning cached queue length

Signed-off-by: andrewhibbert <[email protected]>
…try-After headers and returning cached queue length

Signed-off-by: andrewhibbert <[email protected]>
@andrewhibbert

Copy link
Copy Markdown
Contributor Author

Thanks for adding the event! The event fires on every reconcile while we're rate-limited. That could be a lot of events, because the message includes "last checked at ", so each event is unique, so Kubernetes' event aggregation won't dedupe them. Would it be worth only emitting on state transitions? So once when we go into the rate-limited state and possibly once when we recover? That would also make the event a more meaningful signal.

Further, does EventTypeWarning fit the cache-hit path? The scaler is working as designed there (degraded but functional), so I was wondering if EventTypeNormal might be a better match. The no-cache path does feel like a real warning though. And on that no-cache path, there we're already returning an error which the reconciler surfaces as an event itself, right? So we might be double-emitting there.

The "last checked at " is the last time it last cached inside GetWorkflowQueueLength, so once rate limited this time won't change.

I have changed the event to be EventTypeNormal and removed the no cache event

@rickbrouwer

Copy link
Copy Markdown
Member

Thanks for the explanation. But a separate thing I hadn't spotted before: is "last checked at" correct or is a bit misleading? Would something like "last successful cache at" be more accurate?

…try-After headers and returning cached queue length

Signed-off-by: andrewhibbert <[email protected]>
@andrewhibbert

Copy link
Copy Markdown
Contributor Author

Thanks for the explanation. But a separate thing I hadn't spotted before: is "last checked at" correct or is a bit misleading? Would something like "last successful cache at" be more accurate?

Agreed, I have changed it to "last successful cache at"

@rickbrouwer

rickbrouwer commented Apr 27, 2026

Copy link
Copy Markdown
Member

/run-e2e github*
Update: You can check the progress here

@rickbrouwer rickbrouwer added the Awaiting/2nd-approval This PR needs one more approval review label Apr 27, 2026
@rickbrouwer rickbrouwer self-requested a review April 30, 2026 12:58
@rickbrouwer rickbrouwer removed the Awaiting/2nd-approval This PR needs one more approval review label Apr 30, 2026
@rickbrouwer

rickbrouwer commented Apr 30, 2026

Copy link
Copy Markdown
Member

/run-e2e github*
Update: You can check the progress here

@rickbrouwer

rickbrouwer commented Apr 30, 2026

Copy link
Copy Markdown
Member

/run-e2e github*
Update: You can check the progress here

@rickbrouwer rickbrouwer added the Awaiting/2nd-approval This PR needs one more approval review label Apr 30, 2026
@JorTurFer JorTurFer removed the request for review from a team May 3, 2026 18:18
@JorTurFer JorTurFer enabled auto-merge (squash) May 3, 2026 18:18
auto-merge was automatically disabled May 3, 2026 18:18

Pull request was closed

@JorTurFer

Copy link
Copy Markdown
Member

loooooool. merging kedacore/keda-docs#1749 I've closed directly this one 🤦

@JorTurFer JorTurFer reopened this May 3, 2026
@keda-automation keda-automation requested review from a team May 3, 2026 18:19
@JorTurFer JorTurFer merged commit 5f89276 into kedacore:main May 3, 2026
41 checks passed
@rickbrouwer rickbrouwer removed the Awaiting/2nd-approval This PR needs one more approval review label May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Handle rate limit errors by respecting X-RateLimit-Reset and Retry-After headers and returning cached queue length

3 participants