Skip to main content

Getting started

Rate limits

How requests are budgeted, which headers report the budget, and how to back off.


No numbers on this page are real. The limits, the window, and the header names are all undecided. This page documents the intended mechanism so client code can be structured correctly, not the values to code against.

The bucketing problem

TODO: the current limiter keys purely on IP, which does not work for Roblox. Roblox game servers egress through a shared pool of Roblox-owned addresses, so IP-based limiting would put every experience on the platform into one bucket — your traffic would be throttled by strangers, and a busy neighbour could lock you out entirely.

Per-key bucketing has to land before the public API launches. Options under discussion:

  • Per key
  • Per key, per clan
  • Per key with a separate, higher ceiling for reads

Limits

ScopeLimitWindow
ReadsTODOTODO
WritesTODOTODO
Burst allowanceTODOTODO

Headers

TODO: header names undecided. The intended set, following the RateLimit convention:

HeaderMeaning
RateLimit-LimitRequests permitted in the current window
RateLimit-RemainingRequests left
RateLimit-ResetSeconds until the window resets
Retry-AfterSent on 429 only — seconds to wait

Read Retry-After rather than guessing. It is the only value that accounts for the server's view of your budget.

Backing off

Luau
local HttpService = game:GetService("HttpService")

local function requestWithBackoff(options, attempts)
    attempts = attempts or 4

    for attempt = 1, attempts do
        local response = HttpService:RequestAsync(options)

        if response.StatusCode ~= 429 then
            return response
        end

        -- Honour the server's number when it sends one; otherwise back off
        -- exponentially. The jitter matters: without it every server in the
        -- experience retries on the same tick and arrives as one spike.
        local retryAfter = tonumber(response.Headers["retry-after"])
        local delay = retryAfter or (2 ^ attempt)
        task.wait(delay + math.random() * 0.5)
    end

    return nil
end

Staying under the limit

Batch where you can. One call at the end of a round beats one call per scoring event. XP is additive, so accumulating locally and flushing once is equivalent for the member and far cheaper for you.

Do not poll for data you already have. A game server that just awarded XP already knows the result — it came back in the response. Re-reading the member afterwards doubles your request count for nothing.

Cache reads. Clan and rank structures change rarely. Fetching the rank list once per server start is almost always enough.