Hi there, Palantir-alumni speaking :)
Over the past weeks, policy-bot often exhausted the GitHub App installation primary rate limit (5,000 req/h on GitHub Team) on our stack. Opening this ticket to investigate.
Symptoms
- Details pages returned
500 Internal Server Error (failed to get user permission level → GitHub 403 rate limit).
- PR evaluations failed with
Error loading policy from <org>/<repo>@<branch> and Failed to post repo status.
- The webhook handler returned 503 under load; GitHub-Hookshot then retried, which made the spike worse.
Our setup
- Self-hosted policy-bot (Cloud Run), GitHub App
plume-policy-bot.
- Org on GitHub Team (not Enterprise Cloud) → 5k req/h installation floor.
- Monorepo with Graphite stacked PRs/branches (a mid-stack commit SHA is contained in every branch above it).
- CI via depot.dev: typically ~32 check runs per PR .
.policy.yml uses has_status on a single check, eslint-disable approval (success vs neutral). That is the only status we actually need for policy.
- App is subscribed to
check_run (and other standard events from the README).
A typical event that blows the quota: gt restack on a 20–25 PR stack, which rewrites heads from that point up and starts a full CI matrix on each PR.
What we saw in logs
For 2026-09-02 10:00–11:00 UTC (one restack + CI):
| Signal |
Value |
POST /api/github/hook |
~5,786 |
| Of those, 503 |
~1,936 |
Peak minute (10:00) |
~2,064 webhooks |
| Rate-limit errors that hour |
~588 |
| Dominant handler in error stacks |
CheckRun.Handle |
On a single commit SHA we counted 50–70 Setting "policy-bot" status … lines in ~8 minutes. Hottest SHAs were mid-stack commits from the restacked Graphite stack. GitHub’s “PRs for this commit” list for those SHAs included every PR above that commit.
Webhook volume from check_run completions far exceeded pull_request / review traffic.
Hypothesis (from reading policy-bot source, not a confirmed root-cause analysis)
An agent in our loop read current develop (check_run.go, eval_context.go, base.go). The following reading would explain the numbers — we’d like you to confirm or correct it:
CheckRun.Handle evaluates every PR in check_run.pull_requests.
- Those PRs are “PRs that contain the SHA,” not “PRs whose head is the SHA.” On a Graphite stack, one successful check on a mid-stack commit therefore schedules a full eval for every PR above it.
- Each of those evals uses
TriggerStatus. Because our policy includes has_status, ParseConfig does not skip — so every successful Depot check (lint, tests, …) re-runs the full policy, not only eslint-disable approval, the only check actually referenced by has_status
Base.Evaluate → NewEvalContext (load PR, fetch .policy.yml) runs before the trigger skip, so even a future “policy doesn’t need TriggerStatus” change would still spend API calls per PR per check.
- Rough bound for one full restack, if all 32 checks succeed:
32 × n(n+1)/2 evals (n = stack depth). For n=25 that is ~10k evals, above a 5k installation cap.
We are not claiming this is a bug versus intended behavior, it matches the comment in check_run.go about PRs that contain the SHA. It does make stacked-PR + dense Checks-API CI a hard quota problem on teams.
Soft suggestions
If you agree with the reading, two changes would help a lot (either would have kept us under 5k):
-
Only evaluate PRs whose head SHA equals the check-run head SHA
Skip ancestor / stacked PRs that merely contain the commit. pull_request.synchronize on the rewritten heads would still eval those PRs.
-
Allowlist check names and/or apps for TriggerStatus
e.g. only eslint-disable approval, or only checks that appear in the loaded policy’s has_status / has_workflow predicates. Everything else could no-op before NewEvalContext.
Short-term we will likely unsubscribe check_run and workarroud our need for has_status so we are not status-triggered on every Depot job.
Happy to provide more context if needed, thanks! :)
Hi there, Palantir-alumni speaking :)
Over the past weeks, policy-bot often exhausted the GitHub App installation primary rate limit (5,000 req/h on GitHub Team) on our stack. Opening this ticket to investigate.
Symptoms
500 Internal Server Error(failed to get user permission level→ GitHub403rate limit).Error loading policy from <org>/<repo>@<branch>andFailed to post repo status.Our setup
plume-policy-bot..policy.ymluseshas_statuson a single check,eslint-disable approval(success vs neutral). That is the only status we actually need for policy.check_run(and other standard events from the README).A typical event that blows the quota:
gt restackon a 20–25 PR stack, which rewrites heads from that point up and starts a full CI matrix on each PR.What we saw in logs
For
2026-09-02 10:00–11:00 UTC(one restack + CI):POST /api/github/hook10:00)CheckRun.HandleOn a single commit SHA we counted 50–70
Setting "policy-bot" status …lines in ~8 minutes. Hottest SHAs were mid-stack commits from the restacked Graphite stack. GitHub’s “PRs for this commit” list for those SHAs included every PR above that commit.Webhook volume from
check_runcompletions far exceededpull_request/ review traffic.Hypothesis (from reading policy-bot source, not a confirmed root-cause analysis)
An agent in our loop read current
develop(check_run.go,eval_context.go,base.go). The following reading would explain the numbers — we’d like you to confirm or correct it:CheckRun.Handleevaluates every PR incheck_run.pull_requests.TriggerStatus. Because our policy includeshas_status,ParseConfigdoes not skip — so every successful Depot check (lint, tests, …) re-runs the full policy, not onlyeslint-disable approval, the only check actually referenced byhas_statusBase.Evaluate→NewEvalContext(load PR, fetch.policy.yml) runs before the trigger skip, so even a future “policy doesn’t needTriggerStatus” change would still spend API calls per PR per check.32 × n(n+1)/2evals (n = stack depth). For n=25 that is ~10k evals, above a 5k installation cap.We are not claiming this is a bug versus intended behavior, it matches the comment in
check_run.goabout PRs that contain the SHA. It does make stacked-PR + dense Checks-API CI a hard quota problem on teams.Soft suggestions
If you agree with the reading, two changes would help a lot (either would have kept us under 5k):
Only evaluate PRs whose head SHA equals the check-run head SHA
Skip ancestor / stacked PRs that merely contain the commit.
pull_request.synchronizeon the rewritten heads would still eval those PRs.Allowlist check names and/or apps for
TriggerStatuse.g. only
eslint-disable approval, or only checks that appear in the loaded policy’shas_status/has_workflowpredicates. Everything else could no-op beforeNewEvalContext.Short-term we will likely unsubscribe
check_runand workarroud our need forhas_statusso we are not status-triggered on every Depot job.Happy to provide more context if needed, thanks! :)