Rate limits
Two different things can answer 429 rate_limited: Pealboard’s own limit on
how fast a credential or an address may call this API, and GitHub’s own limit
on how many writes Pealboard’s installation may make in an hour. They have
different remedies.
Pealboard’s own limits
Section titled “Pealboard’s own limits”Counters are per Cloudflare colo and eventually consistent, so each is a
ceiling on abuse rather than a metered quota. None of them carries a
Retry-After header; the response’s remedy says what to do.
| Prefix | Keyed by | Limit | Message |
|---|---|---|---|
/v1/* |
The credential — the session or the API key | 600 requests per 60 seconds | Too many requests. |
/account/*, /auth/*, /invitations/*, /github/* (sign-in and the GitHub install redirect) |
The caller’s address | 60 requests per 60 seconds | Too many attempts from this address. |
/portal/* |
The portal’s host together with the caller’s address | 120 requests per 60 seconds | Too many attempts from this address. |
/attachments/* |
The caller’s address alone — the holder of a link has no credential to key on | 120 requests per 60 seconds | Too many attempts from this address. |
/webhooks/* |
The caller’s address | 3,000 requests per 60 seconds | Too many requests. |
/webhooks/* is Pealboard’s own inbound receiver for GitHub’s deliveries, not
a surface for an integrator to call; it is listed here for completeness. See
GitHub.
A second, narrower limit applies only to the four routes that send a
credential to an address someone typed —
/auth/request-password-reset, /auth/sign-in/magic-link,
/portal/auth/sign-in/magic-link and
/portal/auth/email-otp/send-verification-otp. Each is keyed by the
recipient’s address, not the caller’s, because a source-keyed limit is
defeated by asking from somewhere else and the person harmed by a flood is
the one receiving: 5 requests per 60 seconds, message “Too many messages
have been sent to that address.” This is in addition to the per-source limit
its prefix already carries, not instead of it.
/v1/* is keyed on the credential, not the address, so one script’s own
allowance is never spent by another customer’s traffic on the same network.
Reaching it looks like this:
{ "error": { "code": "rate_limited", "message": "Too many requests.", "remedy": "Slow down and retry. Limits are per credential.", "requestId": "9a1c2e3f-..." }}GitHub’s own limit is the real ceiling on writes
Section titled “GitHub’s own limit is the real ceiling on writes”Reads never touch GitHub — they come from Pealboard’s cache and cost nothing against any budget. A write does one GitHub round trip, and every installation’s writes are serialized through one queue so that a bulk edit cannot race itself into GitHub’s own secondary rate limit.
GitHub gives an installation token 5,000 requests per hour, more for larger
installations, on top of GraphQL’s separate point budget. When that budget is
close to exhausted, or GitHub itself asks Pealboard to slow down (a 403
carrying its own retry-after), a write answers 429 rate_limited with
detail naming why and when it will be safe to retry:
{ "error": { "code": "rate_limited", "message": "This workspace has used its GitHub request budget for the hour.", "detail": { "reason": "exhausted", "retryAt": "2026-09-09T21:00:00.000Z", "remaining": 12, "limit": 5000 }, "remedy": "GitHub resets the budget at 2026-09-09T21:00:00.000Z. Reads from Pealboard's own cache are unaffected.", "requestId": "9a1c2e3f-..." }}detail.reason is one of:
| Reason | Meaning |
|---|---|
exhausted |
The installation has used its hourly GitHub budget. Wait for detail.retryAt. |
paused |
GitHub itself asked Pealboard to slow down. The write is queued and will be retried automatically; nothing was written yet. |
busy |
Too many GitHub operations are already queued for this installation. Retry in a moment. |
There is no Retry-After HTTP header on either kind of 429; a GitHub-budget
refusal carries the same information as detail.retryAt, an ISO 8601 instant,
in the JSON body instead.