Skip to content
WhatIsUp.dev

Rate limits

Token buckets. There are three, they are independent, and any of them can reject a request.

Buckets are keyed by account and channel — not by API key. Issuing a second key does not buy you a second budget; both keys draw from the same customer bucket.

The three buckets

BucketKeyed byBurstRefillApplies to
Per-IP (pre-auth)Client IP3030/sEvery request, checked before your credential is even read
Per-customerYour account601/sEvery authenticated route
Per-channelAccount + channel601/sAny route whose path contains a channel id

The per-IP gate exists so a flood of bad tokens from one address cannot burn our auth path. You will only meet it if you are hammering from a single IP.

What one request costs

One token on each bucket that applies. A call to /v1/channels/{id}/messages spends one customer token and one channel token. A call to /v1/channels spends only a customer token.

The bucket with the fewest tokens left decides the response, and X-RateLimit-Scope tells you which one that was.

So sustained throughput is 1 request/second per channel, with a 60-request burst to absorb spikes. Size your fan-out accordingly: 500 messages on one channel takes roughly 440 seconds, not 500 milliseconds. Spread across channels or pace your sends.

Response headers

Authenticated responses carry:

HeaderMeaning
X-RateLimit-LimitCapacity of the deciding bucket.
X-RateLimit-RemainingTokens left in it after this request was counted.
X-RateLimit-ScopeWhich bucket decided: customer, channel or ip.

On 429 Too Many Requests you also get:

HeaderMeaning
Retry-AfterSeconds to wait. Honour it.

There is no X-RateLimit-Reset header. Earlier revisions of this page documented one; code reading it gets null. Use Retry-After on a 429, or reset_at from GET /v1/limits.

The body is the standard envelope with code: "rate_limited":

{
  "error": {
    "code": "rate_limited",
    "message": "Channel is over the per-second limit (60 burst). Retry in ~4s."
  }
}

Reading your headroom

Pass ?channel_id=<uuid> to fill in per_channel, or authenticate with a channel-bound key and it is filled in automatically:

GET /v1/limits?channel_id=8d653c66-e4ff-43ee-97da-3de5ad5680d4
{
  "per_customer": { "limit": 60, "remaining": 47, "reset_at": "2026-09-10T14:22:31.000Z" },
  "per_channel":  { "limit": 60, "remaining": 12, "reset_at": "2026-09-10T14:22:29.000Z" },
  "channel_id": "8d653c66-e4ff-43ee-97da-3de5ad5680d4",
  "per_key": null
}

A per_channel of null means no channel was identified, not that no channel limit applies. Since a channel send is charged to both buckets, real headroom is min(per_customer.remaining, per_channel.remaining).

per_key is always null — no per-key bucket exists.

This call does spend a token, like every authenticated call — it just does not double-charge for the read itself.

Backoff pattern

async function callApi(url, init) {
  for (let attempt = 0; attempt < 5; attempt++) {
    const res = await fetch(url, init);
    if (res.status !== 429) return res;
 
    // Retry-After is authoritative — it is computed from the actual refill rate.
    const wait = Number(res.headers.get('Retry-After') ?? 1) * 1000;
    await new Promise((r) => setTimeout(r, wait + Math.random() * 250));
  }
  throw new Error('Rate limited after 5 retries');
}

Don't use exponential backoff against a 429 — Retry-After already reflects the refill rate exactly. Reserve exponential backoff for transient 5xx.

What doesn't count

  • Webhook deliveries originate from us and never touch your inbound budget.
  • Public routes/healthz, /readyz, /v1/status/summary, /openapi.json and the signed media proxy /v1/media/{token} — are not charged to your account buckets. The per-IP gate still applies.

Related limits

These are separate caps, not token buckets:

LimitValue
Concurrent /v1/events SSE streams10 per account (429 sse_connection_limit)
Media upload / send size25 MB
Message text length4096 characters
Caption length1024 characters
client_ref length128 characters
Message history page size100

Need more?

Open a ticket (POST /v1/support/tickets) or email hello@whatisup.dev. Higher limits are part of the Pro plan and bespoke for Scale.