> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Troubleshoot push broadcast pre-send gates (BROADCAST_TOO_LARGE, CHANNEL_RATE_LIMITED)

> A whole-org push broadcast (`user_ids: ["*"]`) was refused before dispatch — determine which pre-send gate tripped (audience cap or per-channel rate cap), read the cause from the `details` envelope, fix the gate, and re-send once.

# Troubleshoot push broadcast pre-send gates

`BROADCAST_TOO_LARGE` (422) and `CHANNEL_RATE_LIMITED` (429) are **deterministic pre-send refusals**, not provider failures: your broadcast never reaches APNs / FCM / HMS / Web Push, so nothing is dispatched and no wallet hold is taken. Both gates fire on the whole-org broadcast surface — `user_ids: ["*"]` on `POST /api/v1/push/send`, or `channel: "push"` on the unified `POST /api/v1/messages` pipeline — and both are **tenant-owned controls** you can deliberately size.

This page answers: **which gate fired, where the ceiling is sized, how to read the cause from the `details` envelope, and the one re-send that verifies the fix.**

<Note>
  A push **wildcard** send is `user_ids: ["*"]`, not `device_token_ids`. The audience cap (this page) counts the **deliverable** audience — enabled device tokens minus push/all suppression opt-outs — not the raw row count. Provider-side per-device verdicts (APNs `410` / FCM `UNREGISTERED`, token expiry) return `201` and grade per device in `notifications[]`; decode those on
  [Push token expiry](/troubleshooting/push-token-expiry).
</Note>

## The two gates

| Code | HTTP | Gate | Where the ceiling is sized |
| - | - | - | - |
| `BROADCAST_TOO_LARGE` | 422 | The **audience-size gate** on the dedicated `POST /api/v1/push/send` route: a wildcard (`user_ids: ["*"]`) whose deliverable audience exceeds the platform fan-out ceiling of **100,000 devices**. | Platform constant — never tenant-configurable. Split into explicit `user_ids` lists, or narrow via suppression scope. |
| `CHANNEL_RATE_LIMITED` | 429 | The **per-channel velocity gate** from the cross-channel fraud guard, over a sliding one-minute window. Push shares the `push` channel bucket with every unified-pipeline (`POST /api/v1/messages`) send. | Your per-channel envelope — `GET` / `PUT /api/v1/settings/compliance/channel-rate-overrides` (see [Per-channel rate overrides](/compliance/channel-rate-overrides)), plus the fraud-floor ceiling the [Fraud caps](/compliance/fraud-caps) page lists for `push`. |

Distinguish the envelope a refusal came from:

* **Dedicated push route** (`POST /api/v1/push/send` with `user_ids: ["*"]`) → only `BROADCAST_TOO_LARGE` (audience gate) applies pre-flight. Neither gate fires per-device; per-device `sent`/`skipped`/`failed` rows sit in the `notifications[]` array of a `201`.
* **Unified pipeline** (`POST /api/v1/messages` with `channel: "push"`) → every per-channel fraud gate (rate cap → spend cap → blocklist) runs before the dedicated send executes; `CHANNEL_RATE_LIMITED` surfaces here.

A broadcast that fans out **through the unified pipeline** (for example a `POST /api/v1/messages` fan-out) can also trip the **spend-cap** codes (`SMS_DAILY_SPEND_CAP` / `CHANNEL_DAILY_SPEND_CAP` / `VOICE_DAILY_SPEND_CAP`) on the same request — the per-channel ops ceilings. Those are covered on
[Spend-cap refusals](/troubleshooting/spend-caps-hit); this page covers the rate and audience caps only.

## Cause table

Read the `error.details` envelope from the refusal to scope which lever to pull.

| `BROADCAST_TOO_LARGE` (422) | What the gate counted | Tenant pull |
| - | - | - |
| Audience exceeds 100,000 deliverable devices | The gate pre-counts **deliverable** tokens: `enabled = TRUE` device rows minus active suppression (`channel = 'push'` OR `'all'`) rows — not the raw table size. A row disabled via the per-device `enabled` flag, or a user on the suppression list, never counts. | Split the audience into explicit `user_ids` batches, or narrow it: re-send to a subset (e.g. segment by `user_id` tier) — nothing in the code path truncates silently. |
| Broadcast aborts wholesale | The refusal is **all-or-nothing**: the platform rejects the entire wildcard before any device consumes a slot. | Re-send once with explicit `user_ids` (below) — do not log individual successes. |

| `CHANNEL_RATE_LIMITED` (429) | What the gate counted | Tenant pull |
| - | - | - |
| `push` per-minute cap tripped | A **sliding one-minute window** (not a fixed-minute bucket): current + weighted-previous counts, so a burst at the minute rollover cannot double-fire the cap. Push shares this bucket with every unified `POST /api/v1/messages` send on this tenant. | Throttle the burst, or raise the `push` per-minute override (owner-only) — see |
| [Per-channel rate overrides](/compliance/channel-rate-overrides). | | |
| `details.channel` says `push` | The gate always names its channel so push-throttling never merges silently with SMS/email velocity. | Read `details.channel` + `details.limit` before raising anything — a `sms` refusal must be raised on `sms`, not on `push`. |
| Which channels blocked — from the `details` envelope | Other fraud pushes (mms, whatsapp, rcs, viber, email, telegram, web\_chat, messenger, line, instagram, fax, apple\_messages) each have their own bucket; the envelope's `channel` field identifies which one saturated. | `GET /api/v1/settings/compliance/channel-rate-overrides` returns `overrides` + `defaults` for every channel in one read. |

## Fix table

Fix the gate at **its own** surface, then re-send once.

| Code | Fix | Surface |
| - | - | - |
| `BROADCAST_TOO_LARGE` | Replace the wildcard with explicit `user_ids`; re-send one request. | `POST /api/v1/push/send` (dedicated route) |
| `CHANNEL_RATE_LIMITED` on push | Throttle the burst, or raise the `push` per-minute cap (owner-only) — e.g. `{"overrides": {"push": 500}}` on the whole `overrides` map (PUT **replaces** the map; send all channels you want active). | `PUT /api/v1/settings/compliance/channel-rate-overrides` (whole-map replace) |
| `CHANNEL_RATE_LIMITED` envelope that's really a fraud floor | If the envelope says `channel: "push"` but the value is the **platform floor** (not your override), the fraud guard's env floor is the ceiling — raise it only via the fraud-caps surface (`caps.push.max_per_min`). | `PUT /api/v1/settings/compliance/fraud-caps` (see [Fraud caps](/compliance/fraud-caps)) |

## What NOT to try

* **Retrying the identical payload.** `BROADCAST_TOO_LARGE` never clears itself: the audience gate is a pure function of your token registry — re-running the same wildcard computes the same refusal and burns per-minute rate headroom on the whole API surface. Retry only after the audience is under the cap.
* **Splitting a wildcard into multiple wildcard sends.** Two `user_ids: ["*"]` sends target the same 100,001+ devices and both refuse. Only explicit `user_ids` (or suppression narrowing) reduces the projected count.
* **Looping a 429 retry.** The rate gate is a hard per-minute window — a tight retry loop just spends your own envelope inside the window. Back off ≥ the window (60s) before the first retry after a raise.
* **Assuming the refusal is a provider fault.** Both codes are platform pre-flights; the provider was never called. If you saw `201` with per-device `failed` entries, this page is the wrong runbook — decode per-device verdicts on
  [Push token expiry](/troubleshooting/push-token-expiry).

## Surface the cause

Read the refused response's `error.details` envelope — the `ERROR_CODES` member for both codes declares the same diagnostic shape:

### `BROADCAST_TOO_LARGE` (422)

```json theme={null}
{
  "error": {
    "code": "BROADCAST_TOO_LARGE",
    "message": "Broadcast would target 128044 devices, exceeding the 100000 recipient limit. Use explicit user_ids or segments instead.",
    "status": 422
  },
  "meta": { "request_id": "req_01J9Z8ABCDEF", "timestamp": "2026-09-29T11:00:00Z" }
}
```

The `message` already counts the deliverable audience; the `request_id` is the handle to cite on escalation.

### `CHANNEL_RATE_LIMITED` (429)

```json theme={null}
{
  "error": {
    "code": "CHANNEL_RATE_LIMITED",
    "message": "push rate limit exceeded: 100 sends/min",
    "status": 429,
    "details": { "channel": "push", "limit": 100, "window": "1m" }
  },
  "meta": { "request_id": "req_01J9Z8ABCDEF", "timestamp": "2026-09-29T11:00:00Z" }
}
```

`details.channel` scopes the lever: `push` here — raise `push`, never `sms`. The `window: "1m"` and `limit` values tell you what the sliding window evaluated.

## Recovery sequence

1. **Read the refusal envelope** — confirm the exact `code` and `details.channel` before touching anything.
2. **Fix the gate.**

   * `BROADCAST_TOO_LARGE`: re-send to explicit `user_ids` —

   ```bash theme={null}
   curl -X POST https://api.orbit.devotel.io/api/v1/push/send \
     -H "X-API-Key: $ORBIT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "user_ids": ["user_8a1f2c", "user_9b2e4d", "user_7c3f5a"],
       "title": "Order update",
       "body": "Order #12345 is ready for pickup"
     }'
   ```

   * `CHANNEL_RATE_LIMITED` on push: either throttle the burst and re-send ≥60s later, or raise the `push` override (owner-only) —

   ```bash theme={null}
   curl -X PUT https://api.orbit.devotel.io/api/v1/settings/compliance/channel-rate-overrides \
     -H "X-API-Key: $ORBIT_API_KEY" \
     -H "Content-Type: application/json" \
     -d '{"overrides": {"push": 500}}'
   ```

   Remember: PUT **replaces** the whole `overrides` map — send every channel you want active, or omitted channels revert to the cluster default instantly.
3. **Re-send once.** One clean request after the fix proves the gate re-evaluated; do not parallel-fan the re-send.
4. **Verify.** For the rate gate, `GET /api/v1/settings/compliance/channel-rate-overrides` must show your `push` override alongside the `defaults` entry. For the audience gate, the same payload now returns `201` with per-device results in `notifications[]`.

## When to escalate

Escalate only when the refusal persists **after** the fix on the surface the envelope names. Include:

* Your **tenant ID** (`GET /api/v1/me` → `organizationId`).
* The full **error envelope** — `request_id`, `code`, `message`, and `details` exactly as returned.
* For `BROADCAST_TOO_LARGE`: the **`deliverable device count`** the message reported and the suppression scope in use.
* For `CHANNEL_RATE_LIMITED`: the **current `push` override value** (`GET /api/v1/settings/compliance/channel-rate-overrides`) and the observed send rate.

A 422-versus-429 distinction matters to support: 422 (audience) never clears without a shape change; 429 (rate) clears on the next window once the cap room exists.

## See also

* [Push channel](/channels/push) — the broadcast surface, its common-errors table, and the per-device verdict model.
* [Per-channel rate overrides](/compliance/channel-rate-overrides) — the tenant-owned envelope the `CHANNEL_RATE_LIMITED` gate reads.
* [Fraud caps](/compliance/fraud-caps) — fraud floors, blocks, and the channel velocity model.
* [Spend-cap refusals](/troubleshooting/spend-caps-hit) — the daily-envelope codes a wildcard send can also trip (`SMS_DAILY_SPEND_CAP`, `CHANNEL_DAILY_SPEND_CAP`, `VOICE_DAILY_SPEND_CAP`).
* [Push token expiry](/troubleshooting/push-token-expiry) — per-device provider verdicts inside a `201`.
* [Error codes reference](/reference/error-codes) — the canonical list of every enforcement code.
