Core Concepts
Rate Limits
Kirim applies per-organisation, per-endpoint-class token-bucket rate limits. Limits are enforced atomically by a Redis Lua script — there are no race conditions across replicas.
Endpoint classes
Section titled “Endpoint classes”| Class | HTTP methods | Examples |
|---|---|---|
write | POST, PUT, PATCH, DELETE | Send a message, attach a label, replay a webhook delivery |
read | GET, HEAD | List conversations, fetch a message, introspect via /me |
Each class has its own bucket. Sending 60 messages does not eat into your read budget, and vice versa.
Tier table
Section titled “Tier table”| Plan | Write / min | Read / min |
|---|---|---|
| Default (free) | 60 | 600 |
| Pro | 600 | 6000 |
| Enterprise | configurable | configurable |
Upgrade your plan in the dashboard to lift limits. Enterprise plans get custom per-key overrides on request — contact support.
Headers on every response
Section titled “Headers on every response”Every successful response and every 4xx error carries the same three headers so client code can self-throttle without parsing the body:
X-RateLimit-Limit: 60X-RateLimit-Remaining: 47X-RateLimit-Reset: 1716480000| Header | Meaning |
|---|---|
X-RateLimit-Limit | Bucket capacity for the class you just hit, requests per minute. |
X-RateLimit-Remaining | Tokens currently available (floored to integer). |
X-RateLimit-Reset | Unix epoch seconds when the bucket would be full at the current refill rate. |
On a 429 there is one extra header:
Retry-After: 12Retry-After is the number of seconds until at least one token
becomes available again. Sleep at least that long before retrying.
Handling 429
Section titled “Handling 429”async function callWithBackoff(request: RequestFn, maxAttempts = 5) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { const res = await request() if (res.status !== 429) return res
const retryAfter = Number(res.headers.get('retry-after') ?? 1) // Add jitter so a thundering herd of retries doesn't all wake up // in the same millisecond and trip the limiter again. const jitter = Math.random() * 0.5 + 0.75 // 0.75x ~ 1.25x await sleep(retryAfter * 1000 * jitter) } throw new Error('Rate limit retry budget exhausted')}Anti-patterns
Section titled “Anti-patterns”- Don’t fire requests as fast as
Remainingallows. The bucket refills continuously, but bursting it to zero and immediately retrying just trades a 429 for another 429. - Don’t ignore
Retry-After. It’s accurate. Retrying earlier guarantees a second 429 and consumes attempts you’ll need later. - Don’t share one API key across high-volume consumers. The bucket is per-organisation, not per-key — so two consumers hammering the same org’s key drain the same bucket. Use one key per consumer for clean isolation in audit logs, but accept that they share the budget.
What’s next
Section titled “What’s next”- Idempotency — safely retry without duplicating side effects.
- Errors — full error envelope reference.