Rate limits
Fluxer bounds HTTP API traffic with a per-route bucket and one global bucket. A bucket is one allowance counted over one window. Fluxer denies an over-allowance request with 429, code set to RATE_LIMITED, a rate limit response object, and the rate limit headers.
Buckets and scope
Section titled “Buckets and scope”Nearly every route declares its own bucket. A bucket name can have a path parameter placeholder written as a colon followed by the parameter name, as in guild:emojis:list::guild_id. Fluxer substitutes the request value before keying the bucket, so one route holds a separate allowance for each path resource.
Every bucket is also keyed by the caller’s identity. An authenticated request is keyed by the account and its credential kind, and an OAuth2 bearer credential is keyed by the owning application as well. A session, a bot token, an Admin API key, and each bearer application therefore draw on separate allowances for the same account.
A request that resolves no account is keyed by the client IP address, exactly for IPv4 and by the /64 for IPv6, so clients in the same /64 share an allowance. Where the deployment is configured to read the address from a header the request does not have, Fluxer refuses the request with 403 FORBIDDEN before evaluating any bucket.
A request also counts against the global bucket, unless its route bucket is declared exempt. The global bucket is keyed by the same identity, so a request that resolves no account consumes the global allowance of its client IP address.
These buckets are exempt, and each is the only bucket its route declares: webhook:execute::webhook_id, webhook:message_get::webhook_id, webhook:message_edit::webhook_id, webhook:message_delete::webhook_id, webhook:github::webhook_id, webhook:instatus::webhook_id, and stripe:webhook. Those routes draw on no global allowance. The user:group_dm:create and user:group_dm:recipient:add buckets are exempt as well. Each is a second bucket on a route whose first bucket is not exempt, so both routes still draw on the global allowance.
Every HTTP API and Admin API operation declares a bucket, apart from the desktop download routes, which declare none. A caller that sends no credential on a Bluesky client document is keyed by the client IP address.
The global window is one second. The default allowance is 50 requests per second, and an account holding the HIGH_GLOBAL_RATE_LIMIT flag receives 1,200 requests per second instead. The RATE_LIMIT_BYPASS flag exempts an account from the global bucket and from every route bucket. A successful response to that account has no rate limit header.
Some operations enforce a further limit inside the handler. RATE_LIMIT_BYPASS exempts an account from none of them. Limits enforced inside a handler has the complete set.
Some routes declare a second route bucket, including group direct message creation, adding group recipients, and deleting guild emoji or stickers. Each operation lists its limits. Fluxer counts a request against its route buckets before the handler runs, so a request the handler rejects still uses up allowance.
A deployment can disable both buckets through instance configuration. While they are disabled, no response has an X-RateLimit-* header and no request is refused with 429 RATE_LIMITED. That switch also turns off the login allowances. Every other limit enforced inside a handler stays in force.
Rate limit response object
Section titled “Rate limit response object”The denial body has the members of the ordinary error response and two further members.
Structure
Section titled “Structure”| Field | Type | Description |
|---|---|---|
| code1 | string | RATE_LIMITED on a bucket denial |
| message2 | string | The localised rate limit message |
| global | boolean | Whether the global bucket produced the denial, present and false on a route denial |
| retry_after3 | number | The delay in fractional seconds before another request is admitted |
1 A limit enforced outside the route bucket middleware can reuse this body with its own code. Send phone verification is the only live one, reporting PHONE_RATE_LIMIT_EXCEEDED
2 The locale resolved for the request, which the account setting selects ahead of Accept-Language
3 Never below 0.001, falling back to the whole-second Retry-After value when no fractional delay was computed
The Resend IP authorisation cooldown answers 429 with its own code and a different body. Allowances answering 429 states what that body has.
A slowmode denial answers 400 SLOWMODE_RATE_LIMITED.
Example
Section titled “Example”{ "code": "RATE_LIMITED", "message": "You're being rate limited.", "global": false, "retry_after": 0.428}Rate limit scopes
Section titled “Rate limit scopes”The X-RateLimit-Scope header is the scope that produced a denial.
| Value | Description |
|---|---|
| user | The denial came from an allowance keyed by the caller alone |
| global | The denial came from the global bucket |
| shared1 | The denial came from an allowance that several accounts can exhaust for each other |
1 No route bucket declares a scope of its own, so every route bucket denial reports user. Send phone verification is the only live source of shared
Phone verification reports shared when the per-number send allowance or a number-scoped provider cooldown produced the denial. Both are keyed by the submitted number, so two accounts sending to one number share the allowance.
Rate limit headers
Section titled “Rate limit headers”These headers describe a rate limit decision. An operation that answers 429 returns this set.
| Field | Type | Description |
|---|---|---|
| Retry-After?1 | string | The delay in whole seconds before another request is admitted, rounded up and never below 1 |
| X-RateLimit-Scope?1 | string | The rate limit scope that produced the denial |
| X-RateLimit-Global?2 | string | The literal value true on a global HTTP 429 |
| X-RateLimit-Limit?3 | string | The maximum requests the route allowance admits at once |
| X-RateLimit-Remaining?3 | string | The whole requests the route allowance still admits, always 0 on a denial |
| X-RateLimit-Reset?3 4 | string | The Unix timestamp in seconds at which the whole route allowance is available again |
| X-RateLimit-Reset-After?3 5 | string | The seconds until the whole route allowance is available again |
| X-RateLimit-Bucket?3 6 | string | The stable 16-character hexadecimal identifier of the route bucket |
1 Sent on every rate limit denial and on no successful response
2 Sent only when the global bucket produced the denial, in which case none of the route bucket headers is sent
3 Sent on a route HTTP 429, subject to footnote 6 for X-RateLimit-Bucket. On a successful response they are sent only when the credential resolves to a bot account, or when the request resolves no account at all on a route with both a webhook_id and a token path parameter
4 A value at or before the current second is replaced with the next second
5 Rounded to millisecond precision with trailing zeros removed on a successful response, and emitted as the exact computed decimal on a denial
6 An opaque identifier for the route’s bucket, not the caller
A 429 from a limit enforced inside a handler has the other route headers and no X-RateLimit-Bucket.
Limits enforced inside a handler
Section titled “Limits enforced inside a handler”An allowance enforced inside a handler is keyed independently of the route bucket and of the global bucket, so exhausting it denies the request while both buckets still have room. The set below is complete.
The disable_rate_limits deployment switch turns off the login allowances along with both buckets. relax_registration_rate_limits turns off the registration allowances. Every other allowance below is enforced on every deployment.
A denial takes one of the shapes below. An allowance in Allowances answering 429 answers 429 with the rate limit response object and the rate limit headers minus X-RateLimit-Bucket. An allowance in Allowances answering 400 answers 400 INVALID_FORM_BODY with one validation error entry whose code names the exhausted allowance.
The 400 shape has no retry_after member, no X-RateLimit-* header, and no Retry-After header. The remaining delay appears only in the entry’s localised message.
Allowances answering 429
Section titled “Allowances answering 429”| Operation | Allowance | Code |
|---|---|---|
| Log in with a password | 5 per 15 minutes, keyed by the submitted email address | RATE_LIMITED |
| Log in with a password | 10 per 30 minutes, keyed by the client IP address, exactly for IPv4 and by the /64 for IPv6 | RATE_LIMITED |
| Register an account | 3 per 15 minutes, keyed by the submitted email address | RATE_LIMITED |
| Register an account | 3 per hour, keyed by the client IP address | RATE_LIMITED |
| Register an account | 15 per hour, keyed by the client subnet, the IPv4 /24 or the IPv6 /48 | RATE_LIMITED |
| Resend email verification | 3 per 15 minutes, keyed by the account’s stored email address | RATE_LIMITED |
| Request password recovery | 20 per 30 minutes, keyed by the client IP address | RATE_LIMITED |
| Request password recovery | 5 per 30 minutes, keyed by the submitted email address | RATE_LIMITED |
| Start email change and Resend original email code | 3 sends per 15 minutes, keyed by the authenticated account | RATE_LIMITED |
| Request new email, Resend new email code, and both bounced email recovery sends | 5 sends per 15 minutes, keyed by the authenticated account | RATE_LIMITED |
| Start password change | 3 sends per 15 minutes, keyed by the authenticated account | RATE_LIMITED |
| Resend password change code | 3 sends per 15 minutes, keyed by the authenticated account | RATE_LIMITED |
| Every code resend and every new-address request on an email or password change ticket | 1 send per 30 seconds, keyed by the ticket and counted from its previous send | RATE_LIMITED |
| Report message, Report user, Report guild, and Create DSA report | 5 per hour, keyed by the reporter, an account or a verified email address | RATE_LIMITED |
| Report message | 3 per hour, keyed by the reporter and the channel together | RATE_LIMITED |
| Report message | 20 per hour, keyed by the reported message, across all reporters | RATE_LIMITED |
| Report message | 4 per hour, keyed by the reporter and the guild together, for a guild message | RATE_LIMITED |
| Send phone verification | 3 per 6 hours, keyed by the authenticated account | PHONE_RATE_LIMIT_EXCEEDED |
| Send phone verification | 3 per 5 days, keyed by the submitted number | PHONE_RATE_LIMIT_EXCEEDED |
| Resend IP authorisation | Nothing in the first 30 seconds after the ticket was issued, keyed by the authorisation ticket | IP_AUTHORIZATION_RESEND_COOLDOWN |
SMS provider throttling can impose an additional cooldown. It returns PHONE_RATE_LIMIT_EXCEEDED with the remaining delay.
The Resend IP authorisation cooldown has no X-RateLimit-* header. It has a Retry-After header in whole seconds, and the body reports that delay again as a top-level resend_available_in and retry_after. A second resend on one ticket returns 400 IP_AUTHORIZATION_RESEND_LIMIT_EXCEEDED. The allowance never refills, and the ticket expires 15 minutes after it was issued.
Allowances answering 400
Section titled “Allowances answering 400”| Operation | Allowance | Validation code |
|---|---|---|
| Modify current user | 5 per 3 hours on the resulting username or discriminator | USERNAME_CHANGED_TOO_MANY_TIMES |
| Modify current user | 25 per 30 minutes on the biography, when the submitted value differs | BIO_CHANGED_TOO_MANY_TIMES |
| Modify current user | 25 per 30 minutes on the pronouns, when the submitted value differs | PRONOUNS_CHANGED_TOO_MANY_TIMES |
| Modify current user | 25 per 30 minutes on the accent colour, when the submitted value differs | ACCENT_COLOR_CHANGED_TOO_MANY_TIMES |
| Modify current user | 25 per 30 minutes on any non-null avatar | AVATAR_CHANGED_TOO_MANY_TIMES |
| Modify current user | 25 per 30 minutes on any banner value past the entitlement check | BANNER_CHANGED_TOO_MANY_TIMES |
| Update bot profile | 5 per 3 hours on the bot’s resulting username or discriminator | USERNAME_CHANGED_TOO_MANY_TIMES |
| Modify current guild member | 25 per 30 minutes on the guild avatar, whenever supplied | AVATAR_CHANGED_TOO_MANY_TIMES |
| Modify current guild member | 25 per 30 minutes on the guild banner, whenever supplied | BANNER_CHANGED_TOO_MANY_TIMES |
| Modify current guild member | 25 per 30 minutes on the guild biography, when the submitted value differs | BIO_CHANGED_TOO_MANY_TIMES |
| Modify current guild member | 25 per 30 minutes on the guild pronouns, when the submitted value differs | PRONOUNS_CHANGED_TOO_MANY_TIMES |
| Modify current guild member | 25 per 30 minutes on the guild accent colour, when the submitted value differs | ACCENT_COLOR_CHANGED_TOO_MANY_TIMES |
| Modify voice activity sharing | 1 per 24 hours on the sharing default | VOICE_ACTIVITY_SHARING_ON_COOLDOWN |
| Complete login with TOTP and Complete login with WebAuthn MFA | 10 multi-factor attempts per 15 minutes | INVALID_CODE |
| Complete login with TOTP and Complete login with WebAuthn MFA | 5 multi-factor attempts per 5 minutes on one MFA ticket | INVALID_CODE |
Sudo mode with the totp method | 10 multi-factor attempts per 15 minutes | INVALID_MFA_CODE |
Every Modify current user allowance is keyed by the authenticated account, and the bot tag allowance by the bot account, so an owner changing a bot’s tag draws on the bot’s allowance. The guild member allowances are keyed by the guild and the member together, and one account holds a separate allowance in each guild. The login allowances are keyed by the account and by the MFA ticket respectively, and the sudo allowance by the account.
Fluxer consumes every multi-factor allowance before it checks the code, so a correct code drawn against an exhausted allowance is reported exactly like a wrong one. A correct code clears the counter. The ticket allowance also destroys the MFA ticket as it denies, and the client restarts from Log in with a password.
Allowances answering neither shape
Section titled “Allowances answering neither shape”Get desktop handoff information and Complete desktop handoff share one failed-attempt counter keyed by the client IP address. Five failures block both operations for 15 minutes from the most recent failure, and a blocked request returns 400 INVALID_HANDOFF_CODE as a top-level code with no validation entry. Get desktop handoff information separately permits three successful lookups for each handoff code and reports a fourth with the same top-level code.
Refund latest purchase permits one self-serve refund every 30 days for each account and reports a request inside that window as 403 STRIPE_REFUND_COOLDOWN_ACTIVE.
Slowmode is enforced inside the handler as well.
Slowmode
Section titled “Slowmode”Slowmode limits how often one account sends a message in one channel. Fluxer reports a denial as an ordinary request failure. A denied send returns 400 SLOWMODE_RATE_LIMITED with a top-level retry_after in fractional seconds and a Retry-After header in whole seconds. The response has no X-RateLimit-* header, so a client tells it apart from a bucket denial by the status and the code.
The allowance is one message for each interval the channel configures in rate_limit_per_user, counted separately for each account and channel pair. Fluxer counts it only for a non-bot account sending in a guild channel whose configured interval is above zero. A caller holding BYPASS_SLOWMODE is exempt. Get channel slowmode state reports the caller’s remaining delay before a send is attempted.
Other surfaces
Section titled “Other surfaces”Each protocol surface documents its own rate limit contract. The main Gateway states its session, command, replay, backpressure, and admission limits in Gateway limits and rate limits. The Media Proxy API has no request-count rate limit and bounds work through concurrency, payload, and deadline limits. The upload relay authorises each transfer with an upload URL that expires and limits the body size.