Skip to main content
Rate limits sit in two places, for two different reasons. On the endpoints an attacker can reach without a credential — the ones that guess passwords, enumerate addresses or send mail on somebody else’s behalf — they are a defence, and they count per client IP. Boot is additionally counted per publishable key. On the machine surface, behind a uk_sk_… key, they are a capacity contract. That is the one surface whose caller is a program rather than a person, so it counts per key and publishes the numbers in headers on every response — your client can pace itself instead of discovering the limit by tripping it.

Being limited

429
Retry-After carries the window in seconds. Wait it out; retrying sooner only consumes the next window. On the machine surface the window is a minute, so it is never more than 60.

The limits

Staff authentication

Invitations

Customer plane

The per-key limit on boot exists because the publishable key is the tenant boundary a flood arrives through: bots and landing-page traffic would otherwise mint visitor rows forever.

The contact’s own surface

Per session, not per IP. Behind carrier NAT one address is thousands of people, so an IP bucket on a signed-in surface throttles a crowd for what one of them did — and the session is the thing actually spending the work. /v1/contact/token is why the number exists at all: every call is a key decrypt and a signature.

The machine surface

Per key, not per IP. Your backend calls this from a datacenter address that all of your servers share, and it holds no session — the key is the only thing that identifies the caller, and the only thing you can act on. Two consequences worth planning around:
  • Your own segmentation is your isolation. A key per service — the app, the worker, the nightly job — means a backfill running flat out cannot throttle your sign-in path: its own 1000 stops it before it has spent more than a third of the shared 3000. One key for everything is one bucket for everything.
  • Minting keys does not buy allowance. The environment ceiling is what makes the per-key limit mean anything: the fourth key shares the same 3000.
Live and test never share a ceiling, because the counter is the environment’s. A staging job that spends its whole allowance cannot slow production down. A retry answered from the idempotency table counts like any other request. That is deliberate: a replay is still a round trip, and a client wedged in a redelivery loop is precisely the caller a limit needs to be able to reach.
The rest of the authenticated surface — the panel’s own session — is not throttled beyond the sign-in endpoints above. The credential is the gate, and it is revocable.

Reading the headers

Every response on the machine surface carries the current state, a 200 as much as a 429.
Both families carry the same facts, because there is no single convention: the RateLimit-* names are the IETF draft’s, the X-RateLimit-* names are what most clients written in the last decade already read. Use whichever your HTTP client understands and ignore the other. The one place they differ is the reset, and it is not an oversight — each name means what its own convention says it means. RateLimit-Reset is how many seconds to wait; X-RateLimit-Reset is the timestamp to wait until. X-RateLimit-Scope is there because two ceilings are in play and the numbers describe whichever is closer to running out. RateLimit-Limit: 3000 on a key whose own limit is 1000 means the environment is the binding one: another of your keys is spending the shared allowance.

Failing open, failing closed

Two postures, chosen per endpoint. The counters live in a store shared by every instance of the API. When that store cannot be reached, each instance counts on its own instead — the limits stay, and they get looser rather than disappearing. That is deliberate, and it is the one place this platform does not degrade gracefully. Everything else optional turns off without its dependency; a public sign-up behind no limiter at all is the whole security posture silently off, at exactly the moment an attacker is most likely to be the reason. What you may notice while it lasts: a limit briefly allowing more than the number above, because several instances are each counting to it. Nothing answers differently, and there is nothing to handle. RateLimit-Remaining is then that one instance’s remainder rather than the whole fleet’s, so it reads a little generous. The headers keep coming rather than disappearing for the duration — a contract that vanishes when a dependency does is one no client can branch on.

Staying under them

1

Read the headers on the machine surface

RateLimit-Remaining on the response you already have is cheaper than a 429. A batch job that pauses when it drops below a margin never trips anything.
2

Retry on 429, with backoff

Honour Retry-After when it is present; back off exponentially when it is not.
3

Do not retry a 4xx that is not 429

A 400 or a 403 will answer the same way every time.
4

Debounce boot

One boot per page load, not one per component that wants the contact.
5

Rate-limit your own resend buttons

Verification and magic-link resends are 5 an hour. A button with no cooldown burns them in seconds and the user sees nothing but a failure.