kenari.dev

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#

RequestBucket
POST or PUT /{phone_number_id}/messagesPhone number
POST /{phone_number_id}/mediaPhone number
Everything else on the allow-list: template list/create/delete, media GET/DELETE, number and business profile readsThe WABA's bucket

Limits#

BucketLimitRefill window
Number, standard throughput80 requests/second1 second
Number, high throughput (Meta throughput.level = HIGH)1,000 requests/second1 second
Number in coexistence mode20 requests/second, whatever its tier1 second
WABA5,000 requests/hour1 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:

HeaderValue
RateLimit-LimitThe bucket's capacity: requests per second for a number bucket, requests per hour for a WABA bucket.
RateLimit-RemainingWhole requests left in the bucket after this one.
RateLimit-ResetSeconds, rounded up, until the bucket is completely full again.
Retry-AfterSent only on a 429. Seconds, rounded up, until one request is available.
http
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
HTTP/1.1 429 Too Many RequestsContent-Type: application/jsonRateLimit-Limit: 80RateLimit-Remaining: 0RateLimit-Reset: 1Retry-After: 1X-Kenari-Request-Id: req_…
json
{  "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 limit
  • RateLimit-Remaining: equal to the limit
  • RateLimit-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.