Rate limiting
The request limits on every Landbot API, the 429 response, and how to retry and stay under them.
Every Landbot API accepts up to 40 requests a second. Past that, it answers 429 Too Many Requests, usually with a Retry-After header that says how many seconds to wait.
Limits
| API | Limit | Counted per |
|---|---|---|
| Platform API, Bots API, AI Agents API, Channels API | 40 requests a second | Agent token |
| APIchat | 40 requests a second | Channel token |
Bots API POST /media |
60 uploads an hour, on top of the limit above | Workspace |
AI Agents API writes (POST, PUT, PATCH, DELETE) |
30 an hour, on top of the limit above | Workspace |
Every request counts toward the per-second limit, reads included. The hourly limits are shared by every agent in the workspace.
The 429 response
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 1
{
"detail": "Request was throttled. Expected available in 1 second."
}
Both APIchat and the Platform API answer with this body. Retry-After is a whole number of seconds. It is missing when the wait rounds down to zero, and the body then says Expected available in 0 seconds.
Retry after a 429
Wait for Retry-After, then retry. Fall back to exponential backoff with jitter when the header is missing, and cap the number of attempts and the wait.
async function callWithRetry(url, options, { maxAttempts = 5, maxWaitSeconds = 60 } = {}) {
for (let attempt = 1; ; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429 || attempt >= maxAttempts) return res;
const retryAfter = Number(res.headers.get("retry-after"));
if (retryAfter > maxWaitSeconds) return res;
const wait = retryAfter > 0
? retryAfter * 1000
: Math.min(2 ** attempt * 100, 5000) + Math.random() * 100;
await new Promise(r => setTimeout(r, wait));
}
}
import random, time, requests
def call_with_retry(method, url, *, max_attempts=5, max_wait_seconds=60, **kwargs):
for attempt in range(1, max_attempts + 1):
res = requests.request(method, url, **kwargs)
if res.status_code != 429 or attempt == max_attempts:
return res
retry_after = res.headers.get("Retry-After", "")
if retry_after.isdigit() and int(retry_after) > max_wait_seconds:
return res
if retry_after.isdigit() and int(retry_after) > 0:
wait = int(retry_after)
else:
wait = min(2 ** attempt * 0.1, 5) + random.random() * 0.1
time.sleep(wait)
The jitter spreads retries out when several clients hit the limit at the same moment. On an hourly limit, Retry-After can be many minutes, so both examples return the 429 when the wait is longer than the maximum. Schedule that call for later.
Stay under the limit
For bulk work, throttle on your side:
class RateLimit {
constructor(perSecond = 30) {
this.interval = 1000 / perSecond;
this.last = 0;
}
async wait() {
const now = Date.now();
const delay = Math.max(0, this.last + this.interval - now);
this.last = now + delay;
if (delay) await new Promise(r => setTimeout(r, delay));
}
}
const limit = new RateLimit(30); // headroom under the 40 a second limit
for (const customer of customers) {
await limit.wait();
await sendMessage(customer);
}
Running at about 30 a second leaves room for bursts and for other code that shares the token.
These patterns hit the limit most often:
- Sending to many customers with
Promise.allfrom one process. - Polling
GET /customers/in a loop without a pause. - A webhook handler that makes a Platform API call per message during a burst. Queue those calls and drain them at a steady rate.
If you keep getting 429 for several seconds in a row, you're sending faster than the limit on average. Backoff won't fix that. Lower the rate at the source.