zuplo 6.70.70 → 6.70.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/ai-gateway/getting-started.mdx +2 -1
- package/docs/ai-gateway/integrations/ai-sdk.mdx +17 -0
- package/docs/ai-gateway/introduction.mdx +5 -5
- package/docs/ai-gateway/providers.mdx +2 -0
- package/docs/analytics/access-and-entitlements.md +71 -0
- package/docs/analytics/overview.md +63 -0
- package/docs/analytics/reference/metrics-glossary.md +105 -0
- package/docs/analytics/reference/url-parameters.md +66 -0
- package/docs/analytics/shared-controls.md +121 -0
- package/docs/analytics/tabs/agents.md +88 -0
- package/docs/analytics/tabs/consumers.md +73 -0
- package/docs/analytics/tabs/graphql.md +77 -0
- package/docs/analytics/tabs/mcp.md +80 -0
- package/docs/analytics/tabs/origins.md +82 -0
- package/docs/analytics/tabs/requests.md +96 -0
- package/docs/articles/ci-cd-github/basic-deployment.mdx +10 -1
- package/docs/articles/ci-cd-github/deploy-and-test.mdx +14 -1
- package/docs/articles/ci-cd-github/local-testing.mdx +3 -1
- package/docs/articles/ci-cd-github/pr-preview-environments.mdx +36 -4
- package/docs/articles/custom-ci-cd-github.mdx +11 -2
- package/docs/articles/monetization/api-access.mdx +184 -0
- package/docs/articles/monetization/meters.mdx +4 -4
- package/docs/articles/monetization/monetization-policy.md +4 -1
- package/docs/articles/monetization/private-plans.md +3 -4
- package/docs/articles/monetization/stripe-integration.md +9 -0
- package/docs/articles/monetization/subscription-lifecycle.md +12 -11
- package/docs/articles/monorepo-deployment.mdx +20 -2
- package/docs/cli/deploy.mdx +32 -0
- package/docs/cli/deploy.partial.mdx +32 -0
- package/docs/concepts/api-keys.md +2 -2
- package/docs/dev-portal/zudoku/components/callout.mdx +11 -18
- package/docs/dev-portal/zudoku/configuration/search.md +36 -0
- package/docs/dev-portal/zudoku/configuration/site.md +38 -0
- package/docs/dev-portal/zudoku/customization/colors-theme.mdx +51 -40
- package/docs/errors/rate-limit-exceeded.mdx +30 -3
- package/docs/policies/_index.md +2 -0
- package/docs/policies/data-loss-prevention-inbound/doc.md +116 -0
- package/docs/policies/data-loss-prevention-inbound/intro.md +15 -0
- package/docs/policies/data-loss-prevention-inbound/schema.json +220 -0
- package/docs/policies/data-loss-prevention-outbound/doc.md +116 -0
- package/docs/policies/data-loss-prevention-outbound/intro.md +18 -0
- package/docs/policies/data-loss-prevention-outbound/schema.json +220 -0
- package/docs/programmable-api/background-dispatcher.mdx +6 -8
- package/docs/programmable-api/zone-cache.mdx +1 -1
- package/docs/rate-limiting/combining-policies.mdx +293 -0
- package/docs/rate-limiting/dynamic-rate-limiting.mdx +240 -0
- package/docs/rate-limiting/getting-started.mdx +339 -0
- package/docs/rate-limiting/how-it-works.md +225 -0
- package/docs/rate-limiting/monitoring-and-troubleshooting.mdx +243 -0
- package/docs/{articles → rate-limiting}/per-user-rate-limits-using-db.mdx +39 -27
- package/package.json +4 -4
- package/docs/concepts/rate-limiting.md +0 -246
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Dynamic rate limiting
|
|
3
|
+
sidebar_label: Dynamic rate limiting
|
|
4
|
+
description:
|
|
5
|
+
Learn how to implement dynamic rate limiting with custom functions to apply
|
|
6
|
+
different limits based on customer tier, route, or any request property.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Static rate limits apply the same threshold to every caller. Dynamic rate
|
|
10
|
+
limiting lets you determine limits at request time — so premium customers get
|
|
11
|
+
higher throughput, free-tier users get a lower ceiling, and internal services
|
|
12
|
+
can bypass limits entirely.
|
|
13
|
+
|
|
14
|
+
Dynamic rate limiting works with both the
|
|
15
|
+
[Rate Limiting policy](../policies/rate-limit-inbound.mdx) and the
|
|
16
|
+
[Complex Rate Limiting policy](../policies/complex-rate-limit-inbound.mdx).
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
|
|
20
|
+
When you set `rateLimitBy` to `"function"`, the policy calls a TypeScript
|
|
21
|
+
function you provide on every request. That function returns a
|
|
22
|
+
`CustomRateLimitDetails` object that tells the rate limiter:
|
|
23
|
+
|
|
24
|
+
- **`key`** — The string used to group requests into buckets (e.g., a user ID or
|
|
25
|
+
API key consumer name).
|
|
26
|
+
- **`requestsAllowed`** (optional) — Overrides the policy's default
|
|
27
|
+
`requestsAllowed` for this request.
|
|
28
|
+
- **`timeWindowMinutes`** (optional) — Overrides the policy's default
|
|
29
|
+
`timeWindowMinutes` for this request.
|
|
30
|
+
|
|
31
|
+
Returning `undefined` skips rate limiting for that request entirely.
|
|
32
|
+
|
|
33
|
+
## Create a rate limit function
|
|
34
|
+
|
|
35
|
+
Create a new module (for example `modules/rate-limit.ts`) with a function that
|
|
36
|
+
inspects the request and returns the appropriate limits.
|
|
37
|
+
|
|
38
|
+
The following example reads a `customerType` field from the authenticated user's
|
|
39
|
+
metadata and applies different limits per tier:
|
|
40
|
+
|
|
41
|
+
```ts title="modules/rate-limit.ts"
|
|
42
|
+
import {
|
|
43
|
+
CustomRateLimitDetails,
|
|
44
|
+
ZuploContext,
|
|
45
|
+
ZuploRequest,
|
|
46
|
+
} from "@zuplo/runtime";
|
|
47
|
+
|
|
48
|
+
export function rateLimit(
|
|
49
|
+
request: ZuploRequest,
|
|
50
|
+
context: ZuploContext,
|
|
51
|
+
policyName: string,
|
|
52
|
+
): CustomRateLimitDetails | undefined {
|
|
53
|
+
const user = request.user;
|
|
54
|
+
|
|
55
|
+
// Premium customers get 1000 requests per minute
|
|
56
|
+
if (user.data.customerType === "premium") {
|
|
57
|
+
return {
|
|
58
|
+
key: user.sub,
|
|
59
|
+
requestsAllowed: 1000,
|
|
60
|
+
timeWindowMinutes: 1,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Free customers get 50 requests per minute
|
|
65
|
+
if (user.data.customerType === "free") {
|
|
66
|
+
return {
|
|
67
|
+
key: user.sub,
|
|
68
|
+
requestsAllowed: 50,
|
|
69
|
+
timeWindowMinutes: 1,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Default for any other customer type
|
|
74
|
+
return {
|
|
75
|
+
key: user.sub,
|
|
76
|
+
requestsAllowed: 100,
|
|
77
|
+
timeWindowMinutes: 1,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
:::tip
|
|
83
|
+
|
|
84
|
+
When using [API key authentication](../articles/api-key-authentication.mdx), the
|
|
85
|
+
`user.data` object contains the metadata you set when creating the API key
|
|
86
|
+
consumer. When using JWT authentication, it contains the decoded token claims.
|
|
87
|
+
|
|
88
|
+
:::
|
|
89
|
+
|
|
90
|
+
## Configure the policy
|
|
91
|
+
|
|
92
|
+
Wire the function into the rate limiting policy by setting `rateLimitBy` to
|
|
93
|
+
`"function"` and pointing the `identifier` option at your module:
|
|
94
|
+
|
|
95
|
+
```json title="config/policies.json"
|
|
96
|
+
{
|
|
97
|
+
"name": "my-dynamic-rate-limit-policy",
|
|
98
|
+
"policyType": "rate-limit-inbound",
|
|
99
|
+
"handler": {
|
|
100
|
+
"export": "RateLimitInboundPolicy",
|
|
101
|
+
"module": "$import(@zuplo/runtime)",
|
|
102
|
+
"options": {
|
|
103
|
+
"rateLimitBy": "function",
|
|
104
|
+
"requestsAllowed": 100,
|
|
105
|
+
"timeWindowMinutes": 1,
|
|
106
|
+
"identifier": {
|
|
107
|
+
"export": "rateLimit",
|
|
108
|
+
"module": "$import(./modules/rate-limit)"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The `requestsAllowed` and `timeWindowMinutes` values in the policy configuration
|
|
116
|
+
serve as defaults. Your function can override them per request, or omit them to
|
|
117
|
+
use the defaults.
|
|
118
|
+
|
|
119
|
+
## Common patterns
|
|
120
|
+
|
|
121
|
+
### Tier-based limits from API key metadata
|
|
122
|
+
|
|
123
|
+
Store a `plan` or `customerType` field in your API key consumer metadata, then
|
|
124
|
+
branch on it in your rate limit function. This is the simplest approach and
|
|
125
|
+
requires no external lookups.
|
|
126
|
+
|
|
127
|
+
### Route-based limits
|
|
128
|
+
|
|
129
|
+
Use `request.url` or `request.params` to apply different limits to different
|
|
130
|
+
endpoints. For example, a search endpoint might allow 10 requests per minute
|
|
131
|
+
while a read endpoint allows 100.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
export function rateLimit(
|
|
135
|
+
request: ZuploRequest,
|
|
136
|
+
context: ZuploContext,
|
|
137
|
+
policyName: string,
|
|
138
|
+
): CustomRateLimitDetails | undefined {
|
|
139
|
+
const isSearch = new URL(request.url).pathname.includes("/search");
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
key: request.user.sub,
|
|
143
|
+
requestsAllowed: isSearch ? 10 : 100,
|
|
144
|
+
timeWindowMinutes: 1,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Method-based limits
|
|
150
|
+
|
|
151
|
+
Apply different limits to read operations (GET) vs. write operations (POST, PUT,
|
|
152
|
+
DELETE). Write-heavy endpoints often need tighter limits to protect backends:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
export function rateLimit(
|
|
156
|
+
request: ZuploRequest,
|
|
157
|
+
context: ZuploContext,
|
|
158
|
+
policyName: string,
|
|
159
|
+
): CustomRateLimitDetails | undefined {
|
|
160
|
+
const isWrite = ["POST", "PUT", "DELETE", "PATCH"].includes(request.method);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
key: request.user.sub,
|
|
164
|
+
requestsAllowed: isWrite ? 20 : 200,
|
|
165
|
+
timeWindowMinutes: 1,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Database-driven limits
|
|
171
|
+
|
|
172
|
+
For limits that change frequently or are managed outside your gateway
|
|
173
|
+
configuration, look them up from a database at request time. Use the
|
|
174
|
+
[ZoneCache](../programmable-api/zone-cache.mdx) to avoid hitting the database on
|
|
175
|
+
every request.
|
|
176
|
+
|
|
177
|
+
See
|
|
178
|
+
[Per-user rate limiting with a database](./per-user-rate-limits-using-db.mdx)
|
|
179
|
+
for a complete example using Supabase and ZoneCache.
|
|
180
|
+
|
|
181
|
+
### Skip rate limiting for specific requests
|
|
182
|
+
|
|
183
|
+
Return `undefined` to bypass rate limiting entirely. This is useful for health
|
|
184
|
+
checks, internal services, or admin users:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
export function rateLimit(
|
|
188
|
+
request: ZuploRequest,
|
|
189
|
+
context: ZuploContext,
|
|
190
|
+
policyName: string,
|
|
191
|
+
): CustomRateLimitDetails | undefined {
|
|
192
|
+
if (request.user.data.role === "admin") {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
key: request.user.sub,
|
|
198
|
+
requestsAllowed: 100,
|
|
199
|
+
timeWindowMinutes: 1,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Testing
|
|
205
|
+
|
|
206
|
+
To verify that dynamic limits are applied correctly, create API key consumers
|
|
207
|
+
with different metadata values (for example, one with
|
|
208
|
+
`{"customerType": "premium"}` and one with `{"customerType": "free"}`).
|
|
209
|
+
|
|
210
|
+
Make requests with each key until you receive a `429 Too Many Requests`
|
|
211
|
+
response. For example, with a free-tier key limited to 50 requests per minute:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Replace with your API URL and key
|
|
215
|
+
for i in $(seq 1 55); do
|
|
216
|
+
curl -s -o /dev/null -w "%{http_code}\n" \
|
|
217
|
+
-H "Authorization: Bearer YOUR_API_KEY" \
|
|
218
|
+
https://your-api.zuplo.dev/your-route
|
|
219
|
+
done
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The first 50 requests return `200`. Requests 51-55 return `429` with a
|
|
223
|
+
`Retry-After` header. Repeat with the premium key and confirm the higher limit
|
|
224
|
+
applies.
|
|
225
|
+
|
|
226
|
+
:::tip
|
|
227
|
+
|
|
228
|
+
Rate limit counters are per-environment. Preview and development environments
|
|
229
|
+
have their own counters separate from production, so testing does not affect
|
|
230
|
+
production limits.
|
|
231
|
+
|
|
232
|
+
:::
|
|
233
|
+
|
|
234
|
+
## Related resources
|
|
235
|
+
|
|
236
|
+
- [How rate limiting works](./how-it-works.md) — Full explanation of
|
|
237
|
+
`rateLimitBy` modes and configuration options
|
|
238
|
+
- [Rate Limiting policy reference](../policies/rate-limit-inbound.mdx)
|
|
239
|
+
- [Per-user rate limiting with a database](./per-user-rate-limits-using-db.mdx)
|
|
240
|
+
— Advanced example with database lookups and caching
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Getting started with rate limiting
|
|
3
|
+
sidebar_label: Getting started
|
|
4
|
+
description:
|
|
5
|
+
Pick a rate limiting strategy and add it to an existing Zuplo project, with
|
|
6
|
+
hands-on examples for IP-based and authenticated per-user limits.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Rate limiting caps how many requests a client can make to your API within a time
|
|
10
|
+
window. It protects your backend from traffic spikes, enforces fair usage across
|
|
11
|
+
consumers, and supports tiered access for different customer plans. When a
|
|
12
|
+
client exceeds the configured limit, they receive a `429 Too Many Requests`
|
|
13
|
+
response with a `Retry-After` header indicating when they can retry.
|
|
14
|
+
|
|
15
|
+
This guide walks you through picking a `rateLimitBy` strategy, adding the policy
|
|
16
|
+
to a route, and testing it end to end. If you want the sliding window algorithm,
|
|
17
|
+
every `rateLimitBy` mode in detail, and the full set of configuration levers,
|
|
18
|
+
read [How Rate Limiting Works](./how-it-works.md) alongside or after this guide.
|
|
19
|
+
|
|
20
|
+
## Choose an approach
|
|
21
|
+
|
|
22
|
+
Pick a `rateLimitBy` mode based on what your API looks like today. If you are
|
|
23
|
+
not sure, start from the first row that matches and follow the linked guide or
|
|
24
|
+
section below.
|
|
25
|
+
|
|
26
|
+
| Use case | `rateLimitBy` | Policy | Learn more |
|
|
27
|
+
| ----------------------------------------------------------- | ------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
28
|
+
| Public API with no authentication | `ip` | [Rate Limiting](../policies/rate-limit-inbound.mdx) | Follow the steps below |
|
|
29
|
+
| Authenticated API, same limit for every consumer | `user` | [Rate Limiting](../policies/rate-limit-inbound.mdx) | [§5 Rate limit authenticated users](#5-rate-limit-authenticated-users) |
|
|
30
|
+
| Tiered limits (free, pro, enterprise) from API key metadata | `function` | [Rate Limiting](../policies/rate-limit-inbound.mdx) with a custom function | [Dynamic Rate Limiting](./dynamic-rate-limiting.mdx) |
|
|
31
|
+
| Tiered limits sourced from a database | `function` | [Rate Limiting](../policies/rate-limit-inbound.mdx) with a custom function | [Per-user limits with a database](./per-user-rate-limits-using-db.mdx) |
|
|
32
|
+
| Single global cap on an expensive endpoint | `all` | [Rate Limiting](../policies/rate-limit-inbound.mdx) | [How rate limiting works](./how-it-works.md#all) |
|
|
33
|
+
| Usage-based pricing counting multiple resources per request | `user` | [Complex Rate Limiting](../policies/complex-rate-limit-inbound.mdx) (enterprise) | [How rate limiting works](./how-it-works.md#complex-rate-limiting-policy) |
|
|
34
|
+
|
|
35
|
+
:::note
|
|
36
|
+
|
|
37
|
+
`rateLimitBy: "user"` requires an authentication policy (such as API key or JWT
|
|
38
|
+
authentication) earlier in the route's policy pipeline. Without it, the rate
|
|
39
|
+
limit policy has no user to group requests by and returns an error. Section 5
|
|
40
|
+
below walks through the full authenticated setup.
|
|
41
|
+
|
|
42
|
+
:::
|
|
43
|
+
|
|
44
|
+
For a definition of `rateLimitBy`, the sliding window algorithm, and the full
|
|
45
|
+
list of configuration options (`mode`, `headerMode`, `throwOnFailure`, and
|
|
46
|
+
more), see [How Rate Limiting Works](./how-it-works.md).
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
|
|
50
|
+
- An existing Zuplo project with at least one route configured in
|
|
51
|
+
`config/routes.oas.json`.
|
|
52
|
+
- The [Zuplo CLI](../cli/overview.mdx) installed, or access to the
|
|
53
|
+
[Zuplo Portal](https://portal.zuplo.com).
|
|
54
|
+
- To test rate limiting locally, the project must be linked to a Zuplo
|
|
55
|
+
environment. Run `npx zuplo link` once in the project directory and select an
|
|
56
|
+
environment. Rate limiting uses a globally distributed counter service, so an
|
|
57
|
+
unlinked local project cannot enforce limits. See
|
|
58
|
+
[Connecting to Zuplo Services Locally](../articles/local-development-services.mdx)
|
|
59
|
+
for more detail.
|
|
60
|
+
|
|
61
|
+
## 1. Add the policy
|
|
62
|
+
|
|
63
|
+
Open `config/policies.json` and add a rate limiting policy to the `policies`
|
|
64
|
+
array. This example limits each IP address to 2 requests per minute, which makes
|
|
65
|
+
it easy to test.
|
|
66
|
+
|
|
67
|
+
```json title="config/policies.json"
|
|
68
|
+
{
|
|
69
|
+
"policies": [
|
|
70
|
+
{
|
|
71
|
+
"name": "rate-limit-inbound",
|
|
72
|
+
"policyType": "rate-limit-inbound",
|
|
73
|
+
"handler": {
|
|
74
|
+
"export": "RateLimitInboundPolicy",
|
|
75
|
+
"module": "$import(@zuplo/runtime)",
|
|
76
|
+
"options": {
|
|
77
|
+
"rateLimitBy": "ip",
|
|
78
|
+
"requestsAllowed": 2,
|
|
79
|
+
"timeWindowMinutes": 1
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The key options are:
|
|
88
|
+
|
|
89
|
+
- **`rateLimitBy`** -- How to group requests into rate limit buckets. `"ip"`
|
|
90
|
+
groups by the caller's IP address and requires no authentication.
|
|
91
|
+
- **`requestsAllowed`** -- The maximum number of requests allowed in the time
|
|
92
|
+
window.
|
|
93
|
+
- **`timeWindowMinutes`** -- The length of the sliding time window in minutes.
|
|
94
|
+
|
|
95
|
+
:::tip
|
|
96
|
+
|
|
97
|
+
If your project already has other policies in `config/policies.json`, add the
|
|
98
|
+
rate limiting entry to the existing `policies` array rather than replacing it.
|
|
99
|
+
|
|
100
|
+
:::
|
|
101
|
+
|
|
102
|
+
:::warning
|
|
103
|
+
|
|
104
|
+
The `name` field (`rate-limit-inbound` above) is what scopes the counter. Every
|
|
105
|
+
route that references this exact name shares the same counter. If you later copy
|
|
106
|
+
this policy block to create a second limit, change the `name` — a forgotten
|
|
107
|
+
rename silently merges two unrelated limits into one. Policy names must also
|
|
108
|
+
match exactly between `config/policies.json` and `config/routes.oas.json`; a
|
|
109
|
+
typo there causes the policy to be skipped without any error. See
|
|
110
|
+
[Counter scoping](./combining-policies.mdx#counter-scoping) for the full rules.
|
|
111
|
+
|
|
112
|
+
:::
|
|
113
|
+
|
|
114
|
+
## 2. Attach the policy to a route
|
|
115
|
+
|
|
116
|
+
Open `config/routes.oas.json` and add the policy name to the `policies.inbound`
|
|
117
|
+
array inside the `x-zuplo-route` object of the route you want to protect.
|
|
118
|
+
|
|
119
|
+
```json title="config/routes.oas.json"
|
|
120
|
+
{
|
|
121
|
+
"paths": {
|
|
122
|
+
"/my-route": {
|
|
123
|
+
"get": {
|
|
124
|
+
"operationId": "get-my-route",
|
|
125
|
+
"x-zuplo-route": {
|
|
126
|
+
"corsPolicy": "anything-goes",
|
|
127
|
+
"handler": {
|
|
128
|
+
"export": "urlForwardHandler",
|
|
129
|
+
"module": "$import(@zuplo/runtime)",
|
|
130
|
+
"options": {
|
|
131
|
+
"baseUrl": "https://api.example.com"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"policies": {
|
|
135
|
+
"inbound": ["rate-limit-inbound"]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The `"rate-limit-inbound"` string must match the `name` field from the policy
|
|
145
|
+
you defined in `config/policies.json`. When a request hits this route, Zuplo
|
|
146
|
+
runs each inbound policy in array order before forwarding to the handler.
|
|
147
|
+
|
|
148
|
+
:::note
|
|
149
|
+
|
|
150
|
+
You can attach the same policy to multiple routes. Add its name to the
|
|
151
|
+
`policies.inbound` array on each route that needs rate limiting.
|
|
152
|
+
|
|
153
|
+
:::
|
|
154
|
+
|
|
155
|
+
## 3. Test the rate limit
|
|
156
|
+
|
|
157
|
+
Start your local dev server (or deploy to a Zuplo environment) and send requests
|
|
158
|
+
to the protected route. With the configuration above, the third request within a
|
|
159
|
+
one-minute window returns a `429` response.
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# Send three requests in quick succession
|
|
163
|
+
for i in 1 2 3; do
|
|
164
|
+
echo "--- Request $i ---"
|
|
165
|
+
curl -s -w "\nHTTP Status: %{http_code}\n" http://localhost:9000/my-route
|
|
166
|
+
done
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The first two requests return a `200` response from your upstream service. The
|
|
170
|
+
third request returns a `429 Too Many Requests` response in
|
|
171
|
+
[Problem Details](https://httpproblems.com) format:
|
|
172
|
+
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"type": "https://httpproblems.com/http-status/429",
|
|
176
|
+
"title": "Too Many Requests",
|
|
177
|
+
"status": 429,
|
|
178
|
+
"detail": "Rate limit exceeded",
|
|
179
|
+
"instance": "/my-route",
|
|
180
|
+
"trace": {
|
|
181
|
+
"requestId": "4d54e4ee-c003-4d75-aba9-e09a6d707b08",
|
|
182
|
+
"timestamp": "2026-04-14T12:00:00.000Z",
|
|
183
|
+
"buildId": "ec44e831-3a02-467e-a26c-7e401e4473bf"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The response also includes a `Retry-After` header with the number of seconds
|
|
189
|
+
until the client can send another request (for example, `Retry-After: 42`).
|
|
190
|
+
|
|
191
|
+
## 4. Choose production limits
|
|
192
|
+
|
|
193
|
+
The `requestsAllowed: 2` value above exists so the limit triggers on your third
|
|
194
|
+
curl. Production APIs need numbers that reflect real usage. There is no single
|
|
195
|
+
right answer, but these reference points from widely used APIs are a useful
|
|
196
|
+
starting point:
|
|
197
|
+
|
|
198
|
+
| API | Typical per-consumer limit |
|
|
199
|
+
| ------- | ---------------------------------------------------------- |
|
|
200
|
+
| Stripe | 100 read and 100 write requests per second per account |
|
|
201
|
+
| GitHub | 5,000 authenticated requests per hour per user |
|
|
202
|
+
| Twilio | 100 requests per second per account (varies by resource) |
|
|
203
|
+
| Shopify | 40 requests per app per store (bucket refills at 2/second) |
|
|
204
|
+
|
|
205
|
+
When sizing your own limit, consider three inputs:
|
|
206
|
+
|
|
207
|
+
- **What your backend can sustain.** Start from a conservative fraction of your
|
|
208
|
+
backend's measured capacity so that a single caller cannot exhaust it.
|
|
209
|
+
- **What legitimate callers actually do.** If p99 usage for your best customers
|
|
210
|
+
is 10 requests per minute, a 100-per-minute limit leaves headroom without
|
|
211
|
+
being permissive.
|
|
212
|
+
- **How your customers are structured.** Per-API-key limits usually give tighter
|
|
213
|
+
control than per-IP; a single corporate IP can hide dozens of real users.
|
|
214
|
+
|
|
215
|
+
It is almost always easier to _raise_ a limit in response to a support ticket
|
|
216
|
+
than to _lower_ one that customers have started relying on. When in doubt, start
|
|
217
|
+
low, measure, and increase.
|
|
218
|
+
|
|
219
|
+
## 5. Rate limit authenticated users
|
|
220
|
+
|
|
221
|
+
IP-based limits are a good first layer but they penalize every user behind a
|
|
222
|
+
shared NAT or corporate proxy. For an authenticated API, limit per consumer
|
|
223
|
+
instead. This requires an authentication policy earlier in the pipeline so that
|
|
224
|
+
`request.user` is populated before the rate limit policy runs.
|
|
225
|
+
|
|
226
|
+
The full policies configuration looks like this:
|
|
227
|
+
|
|
228
|
+
```json title="config/policies.json"
|
|
229
|
+
{
|
|
230
|
+
"policies": [
|
|
231
|
+
{
|
|
232
|
+
"name": "api-key-auth",
|
|
233
|
+
"policyType": "api-key-inbound",
|
|
234
|
+
"handler": {
|
|
235
|
+
"export": "ApiKeyInboundPolicy",
|
|
236
|
+
"module": "$import(@zuplo/runtime)",
|
|
237
|
+
"options": {
|
|
238
|
+
"allowUnauthenticatedRequests": false
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
"name": "rate-limit-per-user",
|
|
244
|
+
"policyType": "rate-limit-inbound",
|
|
245
|
+
"handler": {
|
|
246
|
+
"export": "RateLimitInboundPolicy",
|
|
247
|
+
"module": "$import(@zuplo/runtime)",
|
|
248
|
+
"options": {
|
|
249
|
+
"rateLimitBy": "user",
|
|
250
|
+
"requestsAllowed": 60,
|
|
251
|
+
"timeWindowMinutes": 1
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Attach both policies to the route, with authentication first so the rate limit
|
|
260
|
+
policy has a user to group by:
|
|
261
|
+
|
|
262
|
+
```json title="config/routes.oas.json (excerpt)"
|
|
263
|
+
{
|
|
264
|
+
"x-zuplo-route": {
|
|
265
|
+
"policies": {
|
|
266
|
+
"inbound": ["api-key-auth", "rate-limit-per-user"]
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Create two API keys in the Zuplo Portal (or with the CLI) so you can verify that
|
|
273
|
+
each consumer has its own counter. Then send requests with each key:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
# Replace with the tokens from your two API keys.
|
|
277
|
+
KEY_A="zpka_xxxxxxxxxxxxxxxxxxxxxx"
|
|
278
|
+
KEY_B="zpka_yyyyyyyyyyyyyyyyyyyyyy"
|
|
279
|
+
|
|
280
|
+
# Burn through the limit on key A; key B should still succeed.
|
|
281
|
+
for i in $(seq 1 61); do
|
|
282
|
+
curl -s -o /dev/null -w "A #$i: %{http_code}\n" \
|
|
283
|
+
-H "Authorization: Bearer $KEY_A" \
|
|
284
|
+
http://localhost:9000/my-route
|
|
285
|
+
done
|
|
286
|
+
|
|
287
|
+
curl -s -w "\nB #1: %{http_code}\n" \
|
|
288
|
+
-H "Authorization: Bearer $KEY_B" \
|
|
289
|
+
http://localhost:9000/my-route
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Requests 1–60 for key A return `200`, request 61 returns `429`, and the first
|
|
293
|
+
request for key B still returns `200`. That confirms the counter is scoped to
|
|
294
|
+
each consumer, not shared across the API key pool.
|
|
295
|
+
|
|
296
|
+
:::note
|
|
297
|
+
|
|
298
|
+
See [API Key Authentication](../articles/api-key-authentication.mdx) for the
|
|
299
|
+
full walkthrough of creating and managing API keys. If you use JWT
|
|
300
|
+
authentication instead, replace the `api-key-auth` policy with your JWT policy —
|
|
301
|
+
the rate limit policy works the same way as long as `request.user.sub` is
|
|
302
|
+
populated.
|
|
303
|
+
|
|
304
|
+
:::
|
|
305
|
+
|
|
306
|
+
## Next steps
|
|
307
|
+
|
|
308
|
+
**Understand the mechanics:**
|
|
309
|
+
|
|
310
|
+
- [How Rate Limiting Works](./how-it-works.md) — The sliding window algorithm,
|
|
311
|
+
every `rateLimitBy` mode in detail, and advanced options like `mode`,
|
|
312
|
+
`headerMode`, and `throwOnFailure`.
|
|
313
|
+
|
|
314
|
+
**Customize the behavior:**
|
|
315
|
+
|
|
316
|
+
- [Dynamic Rate Limiting](./dynamic-rate-limiting.mdx) — Vary limits per caller
|
|
317
|
+
using a custom TypeScript function (for example, higher limits for paid
|
|
318
|
+
plans).
|
|
319
|
+
- [Per-user limits with a database](./per-user-rate-limits-using-db.mdx) — An
|
|
320
|
+
advanced example using ZoneCache and a database lookup to drive limits per
|
|
321
|
+
customer.
|
|
322
|
+
|
|
323
|
+
**Combine with other policies:**
|
|
324
|
+
|
|
325
|
+
- [Combining Policies](./combining-policies.mdx) — Stack per-minute and per-hour
|
|
326
|
+
limits, pair rate limiting with quotas, and layer in monetization.
|
|
327
|
+
|
|
328
|
+
**Operate in production:**
|
|
329
|
+
|
|
330
|
+
- [Monitoring and Troubleshooting](./monitoring-and-troubleshooting.mdx) —
|
|
331
|
+
Observe limits in production, alert on silent failures, and diagnose
|
|
332
|
+
unexpected 429s.
|
|
333
|
+
|
|
334
|
+
**Reference:**
|
|
335
|
+
|
|
336
|
+
- [Rate Limiting policy reference](../policies/rate-limit-inbound.mdx) — Every
|
|
337
|
+
configuration option for the standard policy.
|
|
338
|
+
- [Complex Rate Limiting policy reference](../policies/complex-rate-limit-inbound.mdx)
|
|
339
|
+
— Multi-counter configuration for usage-based pricing (enterprise).
|