# Throughput and rate limits

Source: https://solidhooks.com/docs/rate-limits

> The three separate limits in SolidHooks, what each one returns, and which are errors and which only slow things down.

"Rate limit" covers three different mechanisms here, and they behave differently. One limits calls to the management API, one limits inbound webhooks, and one paces outbound deliveries. Only the first two ever return an error.

## API requests

An API key (`sdhk_sk_...`) is limited to 600 requests per minute per key. Beyond that the API returns `429`. Back off and retry.

The dashboard's `/auth/*` account endpoints carry their own per-IP limits, which you will not meet when calling `/v1` with a key.

## Inbound webhooks

Two things can reject an inbound request at the ingest origin (`https://sdhk.events/<maskId>`), both before anything is stored.

### Ingest throughput

| Limit              | Scope              | Notes                                                                                                                             |
| ------------------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| Source `rateLimit` | One source         | Requests per second. `0` means unlimited and is the default, so most sources never see it. Read-only on the API: support sets it. |
| Organisation clamp | Whole organisation | Can be applied to a whole organisation. Checked after the source's limit.                                                         |

When either trips, ingest responds `429` with code `RATE_LIMITED`, a body of `data: { limit, remaining, retryAfterSeconds }`, and the headers `Retry-After`, `X-RateLimit-Limit` and `X-RateLimit-Remaining`. The sender still holds the payload, so this is an error on purpose: back off for `Retry-After` seconds and resend.

### Monthly delivery quota

Your plan includes a number of deliveries per usage window. The billable unit is the delivery (one event routed to one connection), so fan-out to three connections counts three. Retries are free, replays are billed, and duplicate (idempotent) requests are never counted.

What happens at the limit depends on the plan:

* **Hard-capped plans** (the free Hobby tier) reject ingest with `429 QUOTA_EXCEEDED` and `data: { limit, current, resetAt }` until the window resets or you upgrade. Event replay is gated the same way.
* **Paid plans with overage enabled** never hard-cap. Deliveries beyond the included quota are metered and billed as pay-as-you-go.

The window is the subscription's billing period for subscribed organisations and the UTC calendar month otherwise. `GET /v1/billing/usage` shows usage against the quota and `resetAt`.

## Outbound delivery pacing

Delivery pacing limits how fast SolidHooks sends to your endpoint. It is never an error. A delivery over the rate is queued and sent when capacity frees up. Nothing is dropped, no retry attempt is consumed, and you do not need to act.

Two limits apply, layered:

* **Plan pacing**: `maxDeliveriesPerSecond` and `burstDeliveriesPerSecond` in the plan's entitlements, across your whole organisation. `null` means unlimited.
* **Destination pacing**: `rateLimit` and `rateLimitPeriod` on a destination, which you set to protect that endpoint. `rateLimitPeriod` is `second`, `minute`, `hour` or `day`.

| Plan       | `maxDeliveriesPerSecond` | `burstDeliveriesPerSecond` |
| ---------- | ------------------------ | -------------------------- |
| Hobby      | 10                       | 20                         |
| Launch     | 50                       | 100                        |
| Scale      | 200                      | 400                        |
| Enterprise | unlimited                | unlimited                  |

```bash
curl -s -X PATCH "$API/v1/destinations/dest_..." \
  -H "authorization: Bearer $SDHK_KEY" \
  -H 'content-type: application/json' \
  -d '{ "data": { "rateLimit": 100, "rateLimitPeriod": "minute" } }'
```

A held delivery has `status: "throttled"` and `throttledReason: "rate_limit"`. This is different from `destination_busy`, which is a delivery parked because your endpoint answered `429`, `502` or `504` (see [Retries](https://solidhooks.com/docs/retries)). A delivery that was ever held keeps a `throttledAt` timestamp, even after it succeeds.

`GET /v1/deliveries/stats` returns the live queue as `{ pending, throttled }`, the number of deliveries waiting right now. Historical outcome counts are in `GET /v1/stats/dashboard`.

> **Retrying a throttled delivery**
>
> `POST /v1/deliveries/{id}/retry` returns `409 CONFLICT` for a `throttled` delivery. It is already queued and will send on its own.

## Connector endpoints

The public connector endpoints (`/v1/connectors/bootstrap` and `/v1/connectors/replicas/heartbeat`) allow 5 requests per second per client IP with a burst of 20, applied before the token is checked. After the token resolves, bootstrap is limited to about one per 5 seconds per connector and heartbeats to one per second per replica. See [Private delivery](https://solidhooks.com/docs/private-delivery).

## Next steps

* [Retries](https://solidhooks.com/docs/retries) — the retry schedule and how responses are interpreted
* [Plans](https://solidhooks.com/docs/plans) — entitlements and quota per plan
* [Errors](https://solidhooks.com/docs/errors) — error codes across the API
* [Destinations](https://solidhooks.com/docs/destinations) — set `rateLimit` on a destination