Skip to content

Survive the limits

The API is free and unauthenticated, so it is bounded. This guide keeps an integration working against those bounds rather than discovering them in production.

Code Retry? What to do
invalid_request No Fix the request. It will fail identically forever.
invalid_formula No Fix the formula, or show the message to whoever wrote it.
too_many_dice No, not unchanged Split the request and send the parts.
payload_too_large No, not unchanged Send fewer rolls per request.
rate_limited Yes, after a pause Back off, then retry the same request.
internal_error Once If it persists, the problem is not yours.

Anything you retry unchanged after a 400 will fail the same way. Only rate_limited and internal_error are worth sending again as they are.

The limit is 60 requests every 60 seconds per address. A 429 means you have spent it.

async function rollWithBackoff(body, attempts = 4) {
for (let attempt = 0; attempt < attempts; attempt++) {
const response = await fetch('https://api.rollful.dev/v1/roll', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
})
if (response.status !== 429) return response
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000))
}
throw new Error('rate limited')
}

Retrying sooner than the window suggests is reasonable here: the limit is counted per Cloudflare location rather than globally, so the budget you are waiting on is not the only one that exists. It also means the ceiling you can reach worldwide is higher than the number above, which is why it is a guard against abuse rather than a quota you can plan around. If you need throughput you can depend on, roll locally with the package instead.

One request may roll 1000 dice, counted across every roll in it rather than per roll. A batch of 20 rolls asking for 100 dice each is 2000 dice and is refused before a single die is thrown.

Count before you send:

// Both bounds are on the limits page. Read them from there rather than from memory.
function chunk(rolls, { maxDice, maxRolls }) {
const batches = []
let batch = []
let dice = 0
for (const roll of rolls) {
const cost = diceIn(roll.formula)
if (batch.length && (dice + cost > maxDice || batch.length === maxRolls)) {
batches.push(batch)
batch = []
dice = 0
}
batch.push(roll)
dice += cost
}
return batch.length ? [...batches, batch] : batches
}

diceIn is yours to write — parseFormula will give you the terms if you are in JavaScript, and a rough count from the digits before each d is enough if you are not. Being approximate is fine; the API does the exact sum and refuses if you are over.

An advantage formula is charged for every die it throws, not for the one it keeps. 4d20adv costs four.

A request body may be 8 KB. You will only meet this with generated batches, since 20 hand-written rolls do not come close. If you are generating formulas, cap the batch on bytes as well as on count.

internal_error means the random source rejected a thousand draws in a row. On a working system that is a one-in-2^1000 event. Retry once; if it happens again, something is wrong here rather than with your request.

  • Limits — every bound and its exact value.
  • Errors — every code and the status it arrives with.