Rate limits#
kenari rate-limits forwarded requests with two kinds of token bucket:
- per phone number, for sends and media uploads, sized from the number's Meta throughput tier
- per WhatsApp Business Account (WABA), for everything else
The limits depend on the number, not on your kenari plan. Plans differ only in how many numbers you can connect.
Which bucket a request uses#
| Request | Bucket |
|---|---|
POST or PUT /{phone_number_id}/messages | Phone number |
POST /{phone_number_id}/media | Phone number |
Everything else on the allow-list: template list/create/delete, media GET/DELETE, number and business profile reads | The WABA's bucket |
Limits#
| Bucket | Limit | Refill window |
|---|---|---|
| Number, standard throughput | 80 requests/second | 1 second |
Number, high throughput (Meta throughput.level = HIGH) | 1,000 requests/second | 1 second |
| Number in coexistence mode | 20 requests/second, whatever its tier | 1 second |
| WABA | 5,000 requests/hour | 1 hour |
The per-number limit comes from the throughput level Meta reports for the number. kenari updates it when it syncs the number's details from Meta.
Buckets refill continuously rather than resetting at fixed moments. A 1,000/s number that is idle has a full bucket and can burst up to 1,000 requests at once, then refills at 1,000/s.
kenari's limits come on top of Meta's own limits. Meta can still reject a send that kenari accepted, and that error reaches you as a normal Meta error.
Response headers#
Every request that reaches the rate-limit step gets these headers, whether it's allowed or refused:
| Header | Value |
|---|---|
RateLimit-Limit | The bucket's capacity: requests per second for a number bucket, requests per hour for a WABA bucket. |
RateLimit-Remaining | Whole requests left in the bucket after this one. |
RateLimit-Reset | Seconds, rounded up, until the bucket is completely full again. |
Retry-After | Sent only on a 429. Seconds, rounded up, until one request is available. |
HTTP/1.1 200 OKContent-Type: application/jsonRateLimit-Limit: 80RateLimit-Remaining: 79RateLimit-Reset: 1X-Kenari-Request-Id: req_…Requests rejected before the rate-limit step (401, 402, 404, 413 and 422) don't have RateLimit-* headers. See the order of checks.
When you hit the limit#
kenari answers 429 without contacting Meta:
HTTP/1.1 429 Too Many RequestsContent-Type: application/jsonRateLimit-Limit: 80RateLimit-Remaining: 0RateLimit-Reset: 1Retry-After: 1X-Kenari-Request-Id: req_…{ "error": { "message": "Rate limit reached for this number.", "type": "KenariThrottleException", "code": 1006, "fbtrace_id": "req_…" }}Wait at least Retry-After seconds before retrying. Buckets refill continuously, so one new request is usually available within a second: a number bucket gains a token at least every second, and the WABA bucket gains one every 0.72 seconds. In practice Retry-After is 1. RateLimit-Reset measures something else, the time until the bucket is completely full again, which for the WABA bucket can be up to 3,600 seconds.
If the limiter is unavailable (fail-open)#
The limiter is built to fail open: if it errors, or takes more than 50 ms to answer, kenari lets the request through instead of rejecting it. You get these headers:
RateLimit-Limit: the bucket's normal limitRateLimit-Remaining: equal to the limitRateLimit-Reset:0
Your traffic keeps flowing during a limiter outage, but kenari isn't pacing it, so Meta's own limits are all that applies. If you see RateLimit-Remaining equal to RateLimit-Limit together with RateLimit-Reset: 0, pace yourself on the client side.