|
| 1 | +--- |
| 2 | +title: Rate limits |
| 3 | +description: > |
| 4 | + Which Layer5 Cloud API routes are rate limited, the limit for each route family, and how to handle HTTP 429. |
| 5 | +weight: 10 |
| 6 | +categories: [reference] |
| 7 | +tags: [extensibility] |
| 8 | +--- |
| 9 | + |
| 10 | +Layer5 Cloud limits how fast a caller can call some API routes. A request over the limit is refused with HTTP 429. The limits first shipped in Layer5 Cloud v1.0.334 and are unchanged through v1.0.342. |
| 11 | + |
| 12 | +Normal desktop use never reaches these limits. You see a 429 only when a client calls far faster than normal use, for example in a retry loop or when it replays a large backlog at once. |
| 13 | + |
| 14 | +## What is limited |
| 15 | + |
| 16 | +Limits apply per caller and per route family. There is no global limit. |
| 17 | + |
| 18 | +- Each route family has its own bucket for each caller. Using up one family never refuses a request in another family. |
| 19 | +- Routes outside the families in the [limits table](#limits-by-route-family) are not rate limited. |
| 20 | +- The families cover the routes the Blowhorn desktop app calls: claims, schedules, ledgers, content items, events, profiles, desktop connections, capabilities, and Blowhorn health. |
| 21 | + |
| 22 | +### Who counts as the caller |
| 23 | + |
| 24 | +The caller is the authenticated user. All of your installs and all of your [tokens]({{< ref "cloud/concepts/identity-and-security/tokens.md" >}}) share your buckets, because they all belong to the same user. |
| 25 | + |
| 26 | +Two kinds of request are counted by client IP address instead: |
| 27 | + |
| 28 | +- A request with no user, such as the capabilities handshake. |
| 29 | +- A request authenticated with the shared anonymous Meshery results token. |
| 30 | + |
| 31 | +Layer5 Cloud checks the `CF-Connecting-IP` header, then `X-Real-IP`, and takes the first valid IPv4 address found across them in that order. If neither header holds a valid IPv4 address, it takes the first valid IPv6 address found. If neither header holds a valid address, it uses the connection's remote address. |
| 32 | + |
| 33 | +## How the limit works |
| 34 | + |
| 35 | +Each bucket is a token bucket with two numbers: |
| 36 | + |
| 37 | +- **Burst** is the size of the bucket: how many requests you can make back to back, starting from a full bucket. |
| 38 | +- **Rate** is how many tokens per second refill the bucket. Each request spends one token. Once the burst is spent, traffic sustained above the rate is refused. |
| 39 | + |
| 40 | +A bucket that is empty refills completely in burst ÷ rate seconds. Over any one minute, a caller can make at most burst + (60 × rate) requests in one family. |
| 41 | + |
| 42 | +A bucket that is not used for 10 minutes is dropped. By then it has refilled completely, so dropping it changes no answer. |
| 43 | + |
| 44 | +## Limits by route family |
| 45 | + |
| 46 | +In the route column, `*` means the path and every path under it. |
| 47 | + |
| 48 | +| Family | Routes | Rate | Burst | |
| 49 | +|---|---|---|---| |
| 50 | +| `claims` | `/api/claims*` | 10 per second | 150 | |
| 51 | +| `schedules` | `/api/schedules*`, `/api/workspaces/{workspaceId}/schedules*` | 20 per second | 200 | |
| 52 | +| `ledgers` | `/api/ledgers*` | 20 per second | 300 | |
| 53 | +| `content_items` | `/api/content-items*` | 20 per second | 400 | |
| 54 | +| `events_write` | `POST /api/events` (run records) | 10 per second | 300 | |
| 55 | +| `events_read` | `GET /api/events*`, `GET /api/workspaces/{workspaceId}/events` | 20 per second | 500 | |
| 56 | +| `events_manage` | `PUT` and `DELETE` on `/api/events*`: `DELETE /api/events`, `DELETE /api/events/{id}`, `PUT /api/events/status`, `PUT /api/events/{id}/status` | 5 per second | 100 | |
| 57 | +| `profiles` | `/api/profiles*`, `/api/workspaces/{workspaceId}/profiles*` | 5 per second | 100 | |
| 58 | +| `desktop_connections` | `POST /api/integrations/connections`, `POST /api/integrations/connections/{connectionId}/heartbeat`, `DELETE /api/integrations/connections/desktop/{installationId}` | 2 per second | 200 | |
| 59 | +| `capabilities` (counted by IP address) | `GET /capabilities` (deprecated), `GET /{meshery-version}/capabilities` | 10 per second | 200 | |
| 60 | +| `blowhorn_health` | `GET /api/blowhorn/health` | 1 per second | 30 | |
| 61 | + |
| 62 | +Registering a connection uses the generic `POST /api/integrations/connections` route. Meshery Server connection registration therefore shares the `desktop_connections` bucket with the desktop app. |
| 63 | + |
| 64 | +### Effective window |
| 65 | + |
| 66 | +| Family | Refill from empty | Most requests in one minute | |
| 67 | +|---|---|---| |
| 68 | +| `claims` | 15 seconds | 750 | |
| 69 | +| `schedules` | 10 seconds | 1,400 | |
| 70 | +| `ledgers` | 15 seconds | 1,500 | |
| 71 | +| `content_items` | 20 seconds | 1,600 | |
| 72 | +| `events_write` | 30 seconds | 900 | |
| 73 | +| `events_read` | 25 seconds | 1,700 | |
| 74 | +| `events_manage` | 20 seconds | 400 | |
| 75 | +| `profiles` | 20 seconds | 400 | |
| 76 | +| `desktop_connections` | 100 seconds | 320 | |
| 77 | +| `capabilities` | 20 seconds | 800 | |
| 78 | +| `blowhorn_health` | 30 seconds | 90 | |
| 79 | + |
| 80 | +### How the limits are sized |
| 81 | + |
| 82 | +The limits are sized for one heavy user running the desktop app on 5 installs at once. Each family has at least 10 times the headroom over that user's normal peak. A script that sends several hundred requests in a few seconds still empties the bucket and is refused. |
| 83 | + |
| 84 | +## When a request is refused |
| 85 | + |
| 86 | +A refused request receives: |
| 87 | + |
| 88 | +- Status `429 Too Many Requests`. |
| 89 | +- A `Retry-After` header: the whole number of seconds until your bucket holds a token again, at least 1. Every family refills at 1 token per second or faster, so with the current limits the value is `1`. |
| 90 | +- A JSON body in the MeshKit error format, with code `meshery_cloud-3629`. |
| 91 | + |
| 92 | +```json |
| 93 | +{ |
| 94 | + "code": "meshery_cloud-3629", |
| 95 | + "error": "Too many requests", |
| 96 | + "severity": "alert", |
| 97 | + "probableCause": [ |
| 98 | + "A client is calling this route family faster than normal use, for example a retry loop or a burst of replayed requests." |
| 99 | + ], |
| 100 | + "suggestedRemediation": [ |
| 101 | + "Wait for the number of seconds in the Retry-After header, then retry. A client that is refused in normal use should pace its requests." |
| 102 | + ] |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +The body does not name the route family. Identify a refusal by the code `meshery_cloud-3629` and the `Retry-After` header, and do not parse the message text. |
| 107 | + |
| 108 | +### Handling a 429 |
| 109 | + |
| 110 | +- Treat 429 as retryable. Wait the number of seconds in `Retry-After`, then retry the request. |
| 111 | +- Retrying a Blowhorn write is safe, because the writes are idempotent: a claim replays, a ledger record is written only if absent, and a run record carries an idempotency key. |
| 112 | +- If your client is refused during normal use, pace its requests so that it stays under the rate of the family it calls. |
| 113 | + |
| 114 | +### Order of checks |
| 115 | + |
| 116 | +The limit is checked after authentication and before the organization of the request is resolved. |
| 117 | + |
| 118 | +- A request with a missing or invalid credential receives HTTP 401 before any rate check, and does not spend a token. |
| 119 | +- A refused request does no work: no organization is resolved and the endpoint does not run. |
| 120 | + |
| 121 | +## Where the counts are kept |
| 122 | + |
| 123 | +- Counts are kept in memory in the Layer5 Cloud server process, separately on each pod. They are not shared between pods and are not saved. |
| 124 | +- Counts reset when the server restarts or a new version is deployed. |
| 125 | +- Hosted Layer5 Cloud runs one replica, so the per-pod count is the real limit. |
| 126 | +- The limits are fixed in the server code. They cannot be configured per organization or per deployment, and a [self-hosted deployment]({{< ref "cloud/guides/self-hosted/_index.md" >}}) has the same values. |
| 127 | + |
| 128 | +For authentication, organization context, and the other error responses, see the [Cloud API]({{< ref "cloud/reference/api-reference/_index.md" >}}). |
0 commit comments