Skip to content

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.

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.