Limits
Every cap in one place — requests per second, per minute, body size, product limits, and the free plan's commercial allowance, including what you see when you exceed each one.
Limits
No limit here is a surprise: the published numbers are frozen by a contract test,
precisely so that whoever integrated against a published number does not start
taking 429s with no explanation. This page is the complete list.
1. Requests per second, per application
Each route class has its own bucket. Exhausting the read bucket does not affect the send bucket.
| Class | Routes | Cap |
|---|---|---|
| Send | POST /api/v1/notifications | 6,000 req/s per application |
| Read | GET /api/v1/notifications/{id} | 1,000 req/s per application |
| Export | POST /api/v1/players/csv_export | 1 req/s per application |
Device routes have no per-second, per-application cap. Registration, updates, receipts, In-App and the Firebase parameters are limited by a different path: the per-origin cap in section 2 and the 50,000 registrations per hour per application cap in section 4. If you are sizing a bulk registration load, those are the two numbers to plan against.
Repeating a read costs no quota
Before the cap is charged, a read is served from a 1-second cache. If your integration asks about the same push twice within the same second, the second answer comes from the cache, does not count against the bucket and never touches the database.
This is deliberate: an integration tracking thousands of pushes per cycle has
every right to ask often, and data one second old has not changed. The cap then
protects what actually costs — the database round trip. Who hits 429 is whoever
asks for different things too fast.
The bucket survives losing the shared cache
With the shared cache up, the count is global per application. If it goes down, the cap still exists, just per service process — degraded, never switched off.
2. Requests per minute, per origin
The public routes called by the device are limited per origin:
| Layer | Cap | Response |
|---|---|---|
| Service | 120 req/min per IP | 429 + Retry-After: 60 |
| Edge | 240 req/min per IP, burst of 60 | 429 |
The edge is deliberately looser than the service: nobody who passes the service is blocked by the edge. It exists as a second line, in case traffic grows faster than the process can refuse it.
Refused requests count too — hammering does not buy attempts.
{
"errors": ["limite de requisições excedido"],
"explain": {
"causa": "mais de 120 req/min a partir do mesmo IP (203.0.113.10)",
"como_corrigir": "aguarde a janela de 60 s e honre o Retry-After",
"request_id": "0198f0b2-6e31-7a4c-9f10-2c9a1d4e7b55"
}
}
The text arrives in Portuguese, exactly as shown. In English: “more than 120 req/min from the same IP” and “wait out the 60 s window and honour Retry-After”.
About the Retry-After header
Be precise when you program your backoff, because the two 429s are not the
same:
- the per-origin
429(device routes) does emitRetry-After: 60; - the per-class
429(send, read, export) carries the wait in theexplain.como_corrigirtext (“aguarde 1s e tente de novo” — wait 1 s and try again) and, in this version, does not add the header.
The safe rule for your client: read Retry-After when it is there; when it is
not, honour the value quoted in explain.como_corrigir. Never retry in a tight
loop.
3. Body size
| Route | Service cap | Edge cap |
|---|---|---|
POST /api/v1/notifications | 256 KB | 256 KB |
Device routes (players, receipts, inapp) | 16 KB | 16 KB |
Other /api/v1 routes | — | 64 KB |
The two caps differ on purpose. 16 KB is the body a device sends (registration, receipt) and it has no reason to grow. The send route carries a recipient list: at the maximum of 2,000 identifiers the request is around 80 KB, and 256 KB leaves three times the headroom over the largest valid batch.
The cap is applied in two stages: if the declared Content-Length already
exceeds it, the response goes out without reading the body; if the actual
body exceeds it (chunked, or a lying Content-Length), it is cut off while
buffering. In both cases the answer is a 413 in the standard envelope, with
the limit named — never a bare 413 with no explanation.
{
"errors": ["corpo acima do limite da rota"],
"explain": {
"causa": "corpo excede o limite da rota (256 KB)",
"como_corrigir": "reduza o corpo da requisição e reenvie",
"request_id": "0198f0b2-6e31-7a4c-9f10-2c9a1d4e7b55"
}
}
4. Product limits
Everything below returns a named 400 or 422, never a silent truncation.
Sending
| Limit | Value |
|---|---|
Identifiers per call (include_player_ids / include_external_user_ids) | 2,000 |
Segments per call (included_segments) | 10 |
| Targets per call | exactly 1 of the three |
ttl | 0 to 2,419,200 s (28 days); default 259,200 s (3 days) |
collapse_id | 64 bytes |
send_after | at most 28 days ahead |
| Rendered payload (what reaches the device) | 3,891 bytes |
The payload limit deserves an explanation, because it surprises people the most.
The 3,891-byte budget is measured on the final payload, including the 96
bytes the server still injects at send time (pm_msg_id and pm_rcpt — the
trail that makes delivery and click receipts possible). If the front door ignored
those bytes, a message right at the limit would pass here and die with a provider
error — the push would vanish silently. The check runs before touching the
database, and the 422 carries the exact count.
Devices
| Limit | Value |
|---|---|
identifier (push token) | 194 bytes |
external_user_id | 128 bytes |
serialized tags | 2 KB |
language | 32 bytes |
timezone | ±86,400 seconds |
Devices per external_user_id | 10 |
| Registrations per hour, per application | 50,000 |
Base export
| Limit | Value |
|---|---|
| Requests | 1 per second, per application |
| Devices in the file | 5,000,000 (sanity cap) |
| Validity of the published file | 72 h |
The export 429 teaches the right path: “the previous file is still valid for
72 h — download the URL you already received instead of asking for another”.
Idempotency
| Limit | Value |
|---|---|
| Idempotency key validity | 30 days |
| Processing reservation before it becomes orphaned | 60 s |
| Wait for a concurrent reservation before the 409 | 3 s |
Thirty days is not decoration: anyone with their own queue reprocesses days later, and a promise that “the same key never fires twice” valid for a single day would not be a promise, it would be luck.
Webhooks
| Limit | Value |
|---|---|
| Your endpoint’s response time | 10 s |
| Attempts per event | 30 (≈ 24 h) |
| Consecutive failures that disable the endpoint | 20 |
Details in Webhooks.
In-App messages
The In-App channel’s limits (blocks per campaign, event batch size, campaigns per session package, package validity and the minimum interval between two messages) live next to the contract that explains them, in In-App messages.
5. The commercial limit: the free plan allowance
This one is not a technical cap, and it is the only limit on this page that refuses the whole send.
The free plan delivers within a monthly active user allowance. Above it,
POST /api/v1/notifications answers 422 and nothing is sent — not even
to part of the list. Truncating would be worse: you would see “campaign sent”,
and part of your base simply would not receive it, with no signal at all.
| What | How it works |
|---|---|
| Where the number lives | in the billing configuration, not hard-coded. The published value today is 1,000 monthly active users, free on any plan — the pricing page is the source of truth |
| Who is measured | the organisation, not each application. Two applications of the same customer share one allowance |
| When it refuses | when the measured number goes above the allowance. Exactly at the allowance still passes |
| Who is never refused | any account on a paid plan. It is not part of the measurement, so no code path can block it |
The response carries the numbers so you can act without opening a ticket:
{
"errors": ["plano Free atende até 1000 usuários ativos no mês — sua conta está com 1240"],
"explain": {
"causa": "o plano Free entrega dentro da franquia de 1000 usuários ativos no mês, e em 2026-08 a sua conta registrou 1240; acima da franquia o envio é recusado INTEIRO — entregar só para uma parte da base, escolhida por ordem de consulta, seria sumir com o resto em silêncio",
"como_corrigir": "mude para o Premium (USD 0.005 por usuário ativo acima da franquia, sem mensalidade) e o envio volta na hora — os 1000 primeiros usuários ativos continuam gratuitos nos dois planos",
"plano": "free",
"mau": 1240,
"mau_gratis": 1000,
"mes": "2026-08",
"moeda": "USD",
"preco_por_mau": "0.005",
"medido_ha_s": 12
}
}
The prose arrives in Portuguese, exactly as printed above. In English, it reads:
“the free plan delivers within an allowance of 1000 monthly active users, and in
2026-08 your account recorded 1240; above the allowance the send is refused
ENTIRELY”, and the fix is “move to the paid plan and sending resumes
immediately — the first 1000 monthly active users stay free on both plans”.
Branch your code on explain.mau, explain.mau_gratis and explain.plano,
never on the sentence.
Three honest notes about that number:
- It is not real time. Counting scans the whole base, so it is recomputed
periodically in the background — at most once a minute — and the
medido_ha_sfield tells you the age of the number you are arguing with. - It is a brake, not an invoice. Between one measurement and the next, an account can go past the allowance for a few minutes and keep delivering. Billing is settled by the invoice, measured over the whole month.
- Infrastructure failure never blocks anyone. With no database, no usable measurement, or a measurement that is too old (over 600 seconds), the check lets the send through. A customer who stops delivering because an internal query failed is a bigger loss than under-billing — and that is a written decision, not an accident.
And the retry rule: 422 belongs to the “fix the request, do not repeat” family.
Here the fix is not in your JSON, it is in the plan — repeating the call changes
nothing.
See also
| Topic | Page |
|---|---|
| the HTTP status table and the retry policy | Authentication and errors |
| every send field and limit | Send a push |
| device route caps in practice | Devices |
| what is frozen by contract and what can change | Versioning |