acyka api

Rate limits

Sixty a minute, six hundred once somebody has vouched for you, and three headers so you never have to guess.

What you get #

per minute
an ordinary application60
one a moderator has marked verified600

Sixty is a number you can build against without asking: a page of the catalogue, a title and its cast is three calls, so a minute of ordinary use is nowhere near it. What it stops is a loop with no sleep in it.

The window is the application and the person, not the token. A token is something a client can mint another of, so keying on one would make “refresh twice a minute” the way round the limit. An application acting for itself has no person, so the whole application is one window — which is right, because there is one of it.

Three headers, on every answer #

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 31

X-RateLimit-Reset is seconds until the window turns, not a timestamp — a clock that disagrees with ours is then not your problem.

A browser client can read all three: they are on Access-Control-Expose-Headers, without which a cross-site page would be able to see the numbers on the wire and not in JavaScript.

And on the refusal #

HTTP/1.1 429 Too Many Requests
Retry-After: 31
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 31

{"message":"common.tooOften"}

Including the 429, which is the half that is easy to forget and the half that matters. A client told only “too many” has nothing to wait for and tries again immediately — which is how a limit turns one impatient caller into a loop against the door.

Retry-After is the same number the limiter is holding, so waiting exactly that long is neither too little (and refused again) nor too much (and asleep for nothing).

What the libraries do about it #

Read the header and wait exactly that long, then try again — up to three times by default. A 429, a 5xx and a socket that never answered are retried; a request refused on its merits is not.

const acyka = Acyka.app(credentials, {
  retries: 3,          // 0 turns retrying off entirely
  maxWait: 65_000,     // past this a RateLimited is raised rather than slept through
  onPace: ({ remaining, reset }) => metrics.gauge('acyka.left', remaining)
});

maxWait exists for a reason worth stating: sleeping a whole window inside one await looks exactly like a hang to whoever is waiting on it. Past that ceiling the RateLimited is raised and the decision is yours.

There is a second ceiling, and it is not about you #

The edge refuses about twenty requests a second from one address, whoever is asking. It is set where no person will ever reach it, because it keys on an address — a carrier's NAT is one bucket for a city — and what it is for is the case where being right about who is beside the point: a flood arriving faster than the api can refuse it, where every refusal has already cost a connection and a database handle.

Asking for more #

The verified mark on an application is what lifts it to six hundred, and it means “this app is who it says it is” rather than “this app is important”. It is asked for the way everything else on this api is: through the account that owns the application.