zuplo 6.70.70 → 6.71.0
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 +67 -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 +122 -0
- package/docs/analytics/tabs/agents.md +88 -0
- package/docs/analytics/tabs/consumers.md +73 -0
- package/docs/analytics/tabs/graphql.md +78 -0
- package/docs/analytics/tabs/mcp.md +80 -0
- package/docs/analytics/tabs/origins.md +83 -0
- package/docs/analytics/tabs/requests.md +97 -0
- package/docs/articles/accounts/enterprise-sso.mdx +8 -6
- package/docs/articles/api-key-administration.mdx +4 -0
- package/docs/articles/bypass-policy-for-testing.mdx +4 -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/environment-variables.mdx +5 -1
- package/docs/articles/environments.mdx +2 -2
- package/docs/articles/graphql.mdx +23 -39
- package/docs/articles/mcp-quickstart-local.mdx +2 -1
- package/docs/articles/mcp-quickstart.mdx +6 -1
- 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/articles/multiple-auth-policies.mdx +2 -2
- package/docs/articles/openapi.mdx +6 -1
- package/docs/articles/rename-or-move-project.mdx +4 -0
- package/docs/articles/securing-your-backend.mdx +11 -3
- package/docs/articles/source-control-setup-github.mdx +4 -0
- package/docs/articles/troubleshooting-slow-responses.mdx +2 -2
- package/docs/articles/troubleshooting.md +3 -3
- 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/dedicated/akamai/architecture.mdx +9 -9
- package/docs/dev-portal/zudoku/components/browser-window.mdx +94 -0
- package/docs/dev-portal/zudoku/components/callout.mdx +11 -18
- package/docs/dev-portal/zudoku/components/landing-page.mdx +283 -0
- 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/handlers/system-handlers.mdx +2 -1
- package/docs/mcp-gateway/how-to/connect-upstream-api-key.mdx +2 -2
- package/docs/mcp-gateway/observability/analytics.mdx +17 -13
- package/docs/mcp-gateway/quickstart.mdx +4 -3
- package/docs/mcp-gateway/troubleshooting.mdx +4 -4
- package/docs/policies/_index.md +3 -0
- package/docs/policies/data-loss-prevention-inbound/doc.md +115 -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 +115 -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/policies/graphql-analytics-outbound/doc.md +93 -0
- package/docs/policies/graphql-analytics-outbound/intro.md +12 -0
- package/docs/policies/graphql-analytics-outbound/schema.json +93 -0
- package/docs/programmable-api/background-dispatcher.mdx +6 -8
- package/docs/programmable-api/zone-cache.mdx +1 -1
- package/docs/programmable-api/zuplo-context.mdx +3 -2
- 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,293 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Combining rate limit policies
|
|
3
|
+
sidebar_label: Combining policies
|
|
4
|
+
description:
|
|
5
|
+
Apply multiple rate limits to the same route, combine rate limiting with
|
|
6
|
+
quotas, and design multi-layer protection strategies.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
Real-world APIs rarely need just one rate limiting boundary. A payment endpoint
|
|
10
|
+
might need a per-minute burst limit to protect against runaway scripts _and_ a
|
|
11
|
+
per-hour cap to enforce fair usage. A monetized API might pair a monthly quota
|
|
12
|
+
with a per-second spike guard. Zuplo supports all of these patterns by letting
|
|
13
|
+
you stack multiple policies on the same route.
|
|
14
|
+
|
|
15
|
+
## Multiple rate limits on one route
|
|
16
|
+
|
|
17
|
+
You can apply two or more rate limiting policies to a single route. Each policy
|
|
18
|
+
maintains its own counter independently, and the request must pass every policy
|
|
19
|
+
to reach the backend.
|
|
20
|
+
|
|
21
|
+
A common pattern is combining a short-window burst limit with a longer-window
|
|
22
|
+
sustained limit. The following example enforces both a 1,000-requests-per-hour
|
|
23
|
+
ceiling and a 100-requests-per-minute burst limit on the same route.
|
|
24
|
+
|
|
25
|
+
### Define the policies
|
|
26
|
+
|
|
27
|
+
```json title="config/policies.json"
|
|
28
|
+
{
|
|
29
|
+
"policies": [
|
|
30
|
+
{
|
|
31
|
+
"name": "rate-limit-hourly",
|
|
32
|
+
"policyType": "rate-limit-inbound",
|
|
33
|
+
"handler": {
|
|
34
|
+
"export": "RateLimitInboundPolicy",
|
|
35
|
+
"module": "$import(@zuplo/runtime)",
|
|
36
|
+
"options": {
|
|
37
|
+
"rateLimitBy": "user",
|
|
38
|
+
"requestsAllowed": 1000,
|
|
39
|
+
"timeWindowMinutes": 60
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "rate-limit-per-minute",
|
|
45
|
+
"policyType": "rate-limit-inbound",
|
|
46
|
+
"handler": {
|
|
47
|
+
"export": "RateLimitInboundPolicy",
|
|
48
|
+
"module": "$import(@zuplo/runtime)",
|
|
49
|
+
"options": {
|
|
50
|
+
"rateLimitBy": "user",
|
|
51
|
+
"requestsAllowed": 100,
|
|
52
|
+
"timeWindowMinutes": 1
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Attach them to a route
|
|
61
|
+
|
|
62
|
+
List both policies in the route's inbound pipeline. Place the longest time
|
|
63
|
+
window first:
|
|
64
|
+
|
|
65
|
+
```json title="config/routes.oas.json (excerpt)"
|
|
66
|
+
{
|
|
67
|
+
"x-zuplo-route": {
|
|
68
|
+
"policies": {
|
|
69
|
+
"inbound": ["rate-limit-hourly", "rate-limit-per-minute"]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
:::tip
|
|
76
|
+
|
|
77
|
+
Apply the longest time window first. If a caller already exhausted the hourly
|
|
78
|
+
quota, the request is rejected immediately without incrementing the per-minute
|
|
79
|
+
counter. This avoids wasting counter writes on requests that would fail anyway.
|
|
80
|
+
|
|
81
|
+
:::
|
|
82
|
+
|
|
83
|
+
Each policy tracks its own sliding window counter scoped by its `name`. A
|
|
84
|
+
request that passes the hourly check still gets evaluated against the per-minute
|
|
85
|
+
check. If either policy rejects the request, the client receives a
|
|
86
|
+
`429 Too Many Requests` response.
|
|
87
|
+
|
|
88
|
+
## Rate limiting vs. quotas
|
|
89
|
+
|
|
90
|
+
Rate limiting and quotas both cap usage, but they solve different problems.
|
|
91
|
+
|
|
92
|
+
| Aspect | Rate limiting | Quota |
|
|
93
|
+
| ----------------- | --------------------------------------------------- | -------------------------------------------- |
|
|
94
|
+
| **Time window** | Short: seconds, minutes, or hours | Long: hourly, daily, weekly, or monthly |
|
|
95
|
+
| **Purpose** | Protect backends from traffic spikes | Enforce billing-period usage caps |
|
|
96
|
+
| **Counter reset** | Sliding window rolls continuously | Fixed period anchored to a start date |
|
|
97
|
+
| **Typical use** | "100 requests per minute per user" | "10,000 requests per month per subscription" |
|
|
98
|
+
| **Policy** | [Rate Limiting](../policies/rate-limit-inbound.mdx) | [Quota](../policies/quota-inbound.mdx) |
|
|
99
|
+
|
|
100
|
+
Use rate limiting when you need to smooth traffic and prevent bursts. Use quotas
|
|
101
|
+
when you need to enforce a usage allowance over a billing cycle. In many APIs,
|
|
102
|
+
you use both together: a monthly quota to cap total usage and a per-minute rate
|
|
103
|
+
limit to prevent any single caller from overwhelming the backend within that
|
|
104
|
+
quota.
|
|
105
|
+
|
|
106
|
+
### Example: quota plus rate limit
|
|
107
|
+
|
|
108
|
+
```json title="config/policies.json"
|
|
109
|
+
{
|
|
110
|
+
"policies": [
|
|
111
|
+
{
|
|
112
|
+
"name": "monthly-quota",
|
|
113
|
+
"policyType": "quota-inbound",
|
|
114
|
+
"handler": {
|
|
115
|
+
"export": "QuotaInboundPolicy",
|
|
116
|
+
"module": "$import(@zuplo/runtime)",
|
|
117
|
+
"options": {
|
|
118
|
+
"period": "monthly",
|
|
119
|
+
"quotaBy": "user",
|
|
120
|
+
"allowances": {
|
|
121
|
+
"requests": 10000
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"name": "burst-rate-limit",
|
|
128
|
+
"policyType": "rate-limit-inbound",
|
|
129
|
+
"handler": {
|
|
130
|
+
"export": "RateLimitInboundPolicy",
|
|
131
|
+
"module": "$import(@zuplo/runtime)",
|
|
132
|
+
"options": {
|
|
133
|
+
"rateLimitBy": "user",
|
|
134
|
+
"requestsAllowed": 100,
|
|
135
|
+
"timeWindowMinutes": 1
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
On the route, place the quota policy first so that callers who already used
|
|
144
|
+
their monthly allowance are rejected before the rate limit counter is
|
|
145
|
+
incremented:
|
|
146
|
+
|
|
147
|
+
```json title="config/routes.oas.json (excerpt)"
|
|
148
|
+
{
|
|
149
|
+
"x-zuplo-route": {
|
|
150
|
+
"policies": {
|
|
151
|
+
"inbound": ["monthly-quota", "burst-rate-limit"]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Rate limiting with monetization
|
|
158
|
+
|
|
159
|
+
The [Monetization policy](../articles/monetization/monetization-policy.md)
|
|
160
|
+
handles subscription validation, quota enforcement, and metering in one step. It
|
|
161
|
+
already enforces billing-period usage limits tied to the customer's plan, so you
|
|
162
|
+
do not need a separate quota policy on monetized routes.
|
|
163
|
+
|
|
164
|
+
Rate limiting is still valuable alongside monetization. A customer with a 50,000
|
|
165
|
+
requests-per-month plan could theoretically send all 50,000 requests in a single
|
|
166
|
+
minute, which would overwhelm your backend even though it falls within the
|
|
167
|
+
monthly allowance. Adding a rate limiting policy prevents that spike.
|
|
168
|
+
|
|
169
|
+
```json title="config/routes.oas.json (excerpt)"
|
|
170
|
+
{
|
|
171
|
+
"x-zuplo-route": {
|
|
172
|
+
"policies": {
|
|
173
|
+
"inbound": ["monetization-inbound", "rate-limit-per-minute"]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
:::note
|
|
180
|
+
|
|
181
|
+
The monetization policy handles API key authentication internally. You do not
|
|
182
|
+
need a separate `api-key-auth` policy on monetized routes. Place the
|
|
183
|
+
monetization policy first so that `request.user` is populated before the rate
|
|
184
|
+
limit policy runs.
|
|
185
|
+
|
|
186
|
+
:::
|
|
187
|
+
|
|
188
|
+
These two layers are complementary:
|
|
189
|
+
|
|
190
|
+
- **Monetization** enforces monthly or billing-period usage limits and tracks
|
|
191
|
+
metered usage for billing.
|
|
192
|
+
- **Rate limiting** enforces per-minute or per-second spike protection to keep
|
|
193
|
+
your backend healthy.
|
|
194
|
+
|
|
195
|
+
## Counter scoping
|
|
196
|
+
|
|
197
|
+
Rate limit counters are scoped by the policy's `name` field combined with the
|
|
198
|
+
caller identifier (user, IP, or custom key). Understanding this scoping is
|
|
199
|
+
important when you apply the same policy type to multiple routes.
|
|
200
|
+
|
|
201
|
+
### Shared counters
|
|
202
|
+
|
|
203
|
+
If two routes reference the same policy name, they share a counter. A caller who
|
|
204
|
+
makes 60 requests to `/orders` and 40 requests to `/products` — both using a
|
|
205
|
+
policy named `rate-limit-per-minute` — counts as 100 total requests against that
|
|
206
|
+
policy's limit.
|
|
207
|
+
|
|
208
|
+
```json title="config/routes.oas.json (excerpt)"
|
|
209
|
+
{
|
|
210
|
+
"paths": {
|
|
211
|
+
"/orders": {
|
|
212
|
+
"get": {
|
|
213
|
+
"x-zuplo-route": {
|
|
214
|
+
"policies": {
|
|
215
|
+
"inbound": ["rate-limit-per-minute"]
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
"/products": {
|
|
221
|
+
"get": {
|
|
222
|
+
"x-zuplo-route": {
|
|
223
|
+
"policies": {
|
|
224
|
+
"inbound": ["rate-limit-per-minute"]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Shared counters are useful when you want a single global limit that applies
|
|
234
|
+
across all routes for a given caller.
|
|
235
|
+
|
|
236
|
+
### Independent counters
|
|
237
|
+
|
|
238
|
+
To give each route its own counter, create separate policy instances with
|
|
239
|
+
different names:
|
|
240
|
+
|
|
241
|
+
```json title="config/policies.json"
|
|
242
|
+
{
|
|
243
|
+
"policies": [
|
|
244
|
+
{
|
|
245
|
+
"name": "rate-limit-orders",
|
|
246
|
+
"policyType": "rate-limit-inbound",
|
|
247
|
+
"handler": {
|
|
248
|
+
"export": "RateLimitInboundPolicy",
|
|
249
|
+
"module": "$import(@zuplo/runtime)",
|
|
250
|
+
"options": {
|
|
251
|
+
"rateLimitBy": "user",
|
|
252
|
+
"requestsAllowed": 100,
|
|
253
|
+
"timeWindowMinutes": 1
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
"name": "rate-limit-products",
|
|
259
|
+
"policyType": "rate-limit-inbound",
|
|
260
|
+
"handler": {
|
|
261
|
+
"export": "RateLimitInboundPolicy",
|
|
262
|
+
"module": "$import(@zuplo/runtime)",
|
|
263
|
+
"options": {
|
|
264
|
+
"rateLimitBy": "user",
|
|
265
|
+
"requestsAllowed": 200,
|
|
266
|
+
"timeWindowMinutes": 1
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Now a caller can make 100 requests per minute to `/orders` and 200 requests per
|
|
275
|
+
minute to `/products` independently. Exhausting the orders limit does not affect
|
|
276
|
+
the products limit.
|
|
277
|
+
|
|
278
|
+
:::warning
|
|
279
|
+
|
|
280
|
+
If you duplicate a policy definition and forget to change the `name`, both
|
|
281
|
+
routes share the same counter. Always verify that policy names are distinct when
|
|
282
|
+
you intend independent counters.
|
|
283
|
+
|
|
284
|
+
:::
|
|
285
|
+
|
|
286
|
+
## Related resources
|
|
287
|
+
|
|
288
|
+
- [Rate Limiting policy reference](../policies/rate-limit-inbound.mdx)
|
|
289
|
+
- [Complex Rate Limiting policy reference](../policies/complex-rate-limit-inbound.mdx)
|
|
290
|
+
- [Quota policy reference](../policies/quota-inbound.mdx)
|
|
291
|
+
- [Monetization policy](../articles/monetization/monetization-policy.md)
|
|
292
|
+
- [How rate limiting works](./how-it-works.md)
|
|
293
|
+
- [Dynamic Rate Limiting](./dynamic-rate-limiting.mdx)
|
|
@@ -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
|