> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bloxcord.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Best Practices

> How to use the Game API efficiently and reliably — rate limits, caching, batching, and error handling.

The Game API is fast, but every call is a network request that shares a per-server
budget. A few habits keep your experience responsive and prevent your calls from
being throttled or dropped. Read this once before shipping.

## Respect the rate limits

Every operation — single calls, batched calls, and reads — draws from one
per-server budget:

| Limit                                                     | Value   |
| --------------------------------------------------------- | ------- |
| Operations per minute (per server)                        | **120** |
| Operations per [`apiBatch`](/reference/webhooks#apibatch) | **20**  |

When you exceed the budget, calls start returning `nil`/`false` (HTTP `429`).
Some expensive methods also carry their own, tighter limit — for example
[`getLeaderboard`](/reference/activity#getleaderboard) allows **one call every 30
seconds** per server. These are noted on the relevant method.

<Warning>
  Budgets are **per game server**, not per player. In a full 50-player server,
  "one call per player" can blow the entire minute budget in seconds. Always think
  in terms of the whole server.
</Warning>

## Don't call the same endpoint repeatedly

The single biggest cause of throttling is re-fetching data that hasn't changed.
Fetch it once, keep the result, and reuse it.

<Warning>
  Never put an API call inside a loop that runs every frame or tick — a
  `RunService.Heartbeat`/`Stepped`/`RenderStepped` connection, a `while true`
  loop, or a per-hit combat handler. Even a "cheap" read will exhaust the budget
  and stall the calling thread (every call [yields](/api/concepts#yielding)).
</Warning>

Fetch on the event that actually changes the data instead:

```lua theme={null}
local BloxCord = game:WaitForChild("BloxCord")

-- GOOD: fetch a player's permissions once, when they join.
game.Players.PlayerAdded:Connect(function(player)
    local permissions = BloxCord:Invoke("getUserPermissions", player)
    player:SetAttribute("BC_Loaded", permissions ~= nil)
end)
```

```lua theme={null}
-- BAD: re-fetching the same unchanging data on every command.
local function onCommand(player)
    local permissions = BloxCord:Invoke("getUserPermissions", player) -- ❌ every time
    -- ...
end
```

## Cache reads in your own code

For data you read often, keep a small in-memory cache keyed by user id and refresh
it only when it matters (on a timer, or after you change the underlying data).

```lua theme={null}
local BloxCord = game:WaitForChild("BloxCord")

local cache = {}           -- [userId] = { data = ..., expires = os.clock() + ttl }
local TTL = 60             -- seconds

local function getProfile(userId)
    local entry = cache[userId]
    if entry and os.clock() < entry.expires then
        return entry.data
    end

    local profile = BloxCord:Invoke("getUserProfile", userId)
    if profile then
        cache[userId] = { data = profile, expires = os.clock() + TTL }
    end
    return profile
end

game.Players.PlayerRemoving:Connect(function(player)
    cache[player.UserId] = nil -- evict on leave
end)
```

<Note>
  A few methods are already cached for you per player for the length of their
  session — [`getUserPermissions`](/reference/ranking-permissions#getuserpermissions)
  and [`getUserRoles`](/reference/ranking-permissions#getuserroles). You still
  shouldn't call them in a loop, but you don't need to build your own cache for
  them.
</Note>

## Batch related operations

If you need to perform several operations at once, send them together with
[`apiBatch`](/reference/webhooks#apibatch) instead of firing many separate calls.
One batch of 20 counts the same against the budget as 20 singles, but it's a single
round-trip — faster, and far less likely to interleave with other work.

```lua theme={null}
local BloxCord = game:WaitForChild("BloxCord")

-- One request instead of three.
local results = BloxCord:Invoke("apiBatch", host, {
    { op = "points.update",   args = { userId = "111", amount = 5, reason = "Attendance" } },
    { op = "points.update",   args = { userId = "222", amount = 5, reason = "Attendance" } },
    { op = "logbook.create",  args = { userId = "333", type = "warning", reason = "Late" } },
})
```

<Tip>
  Award end-of-session points, mark attendees, and write logs in a single batch
  at the end rather than one call per player as things happen.
</Tip>

## Debounce player-triggered actions

Any call a player can trigger (a button, a command, a chat trigger) needs a guard,
or a single player can spam your budget. Track a per-player timestamp and reject
calls that come too fast.

```lua theme={null}
local lastUsed = {} -- [userId] = os.clock()
local COOLDOWN = 3  -- seconds

local function handleRequest(player)
    local now = os.clock()
    if lastUsed[player.UserId] and now - lastUsed[player.UserId] < COOLDOWN then
        return -- ignore spam
    end
    lastUsed[player.UserId] = now

    -- safe to make the call here
end
```

## Handle failures without hammering

A `nil` or `false` return can mean a rejection *or* a transient network issue.
Always check the result before using it, and if you retry, **back off** — never
retry in a tight loop.

```lua theme={null}
local BloxCord = game:WaitForChild("BloxCord")

local function withRetry(fn, attempts)
    for i = 1, attempts do
        local result = fn()
        if result ~= nil and result ~= false then
            return result
        end
        task.wait(2 ^ i) -- 2s, 4s, 8s ... exponential backoff
    end
    return nil
end

local balance = withRetry(function()
    return BloxCord:Invoke("getUserPoints", requester, player)
end, 3)
```

<Warning>
  Do not retry immediately or in a loop. If you were throttled, retrying instantly
  keeps you throttled and can starve every other call in the server.
</Warning>

## Run independent calls concurrently — within reason

Because each call yields, running several **independent** calls sequentially adds
up their wait times. Use `task.spawn` to overlap them — but don't fan out hundreds
at once, or you'll hit the budget instantly.

```lua theme={null}
-- Overlap a handful of independent lookups.
for _, player in ipairs(game.Players:GetPlayers()) do
    task.spawn(function()
        local mins = BloxCord:Invoke("getUserMinutes", player.UserId, {})
        player:SetAttribute("BC_Minutes", mins and mins.active or 0)
    end)
end
```

<Tip>
  For a value you need for *many* users at once, prefer a method that returns them
  together (e.g. [`getLeaderboard`](/reference/activity#getleaderboard) with a
  `userIds` list) over one call per user.
</Tip>

## Keep payloads small and reuse identifiers

* Each operation's `args` payload is capped at **24 KB**. Send only what the
  method needs — don't forward large tables or entire data models.
* When you start a session, keep the returned `sid` and pass it to every follow-up
  call rather than fetching the session again to find it.
* Remember timestamps are **Unix epoch milliseconds** (see
  [Core Concepts → Timestamps](/api/concepts#timestamps)); `os.time()` returns
  seconds, so multiply by `1000`.

## Quick checklist

<Check>No API calls inside per-frame or `while true` loops.</Check>
<Check>Reads that don't change are cached (or fetched once on join).</Check>
<Check>Multiple operations are grouped into a single `apiBatch`.</Check>
<Check>Player-triggered actions are debounced.</Check>
<Check>Every result is checked for `nil`/`false` before use.</Check>
<Check>Retries use backoff, never a tight loop.</Check>

## Next steps

<CardGroup cols={2}>
  <Card title="Core Concepts" icon="book" href="/api/concepts">
    Actor object, return conventions, yielding, and the full rate-limit rules.
  </Card>

  <Card title="apiBatch" icon="layer-group" href="/reference/webhooks#apibatch">
    Run several operations in a single request.
  </Card>
</CardGroup>
