zuplo 6.70.69 → 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.
Files changed (93) hide show
  1. package/docs/ai-gateway/getting-started.mdx +14 -9
  2. package/docs/ai-gateway/integrations/ai-sdk.mdx +17 -0
  3. package/docs/ai-gateway/introduction.mdx +12 -10
  4. package/docs/ai-gateway/providers.mdx +2 -0
  5. package/docs/analytics/access-and-entitlements.md +71 -0
  6. package/docs/analytics/overview.md +63 -0
  7. package/docs/analytics/reference/metrics-glossary.md +105 -0
  8. package/docs/analytics/reference/url-parameters.md +66 -0
  9. package/docs/analytics/shared-controls.md +121 -0
  10. package/docs/analytics/tabs/agents.md +88 -0
  11. package/docs/analytics/tabs/consumers.md +73 -0
  12. package/docs/analytics/tabs/graphql.md +77 -0
  13. package/docs/analytics/tabs/mcp.md +80 -0
  14. package/docs/analytics/tabs/origins.md +82 -0
  15. package/docs/analytics/tabs/requests.md +96 -0
  16. package/docs/articles/api-key-buckets.mdx +4 -2
  17. package/docs/articles/archiving-requests-to-storage.mdx +4 -4
  18. package/docs/articles/branch-based-deployments.mdx +10 -8
  19. package/docs/articles/ci-cd-github/basic-deployment.mdx +10 -1
  20. package/docs/articles/ci-cd-github/cleanup-on-branch-delete.mdx +52 -31
  21. package/docs/articles/ci-cd-github/deploy-and-test.mdx +14 -1
  22. package/docs/articles/ci-cd-github/local-testing.mdx +3 -1
  23. package/docs/articles/ci-cd-github/pr-preview-environments.mdx +53 -10
  24. package/docs/articles/custom-ci-cd-azure.mdx +1 -1
  25. package/docs/articles/custom-ci-cd-bitbucket.mdx +1 -1
  26. package/docs/articles/custom-ci-cd-circleci.mdx +1 -1
  27. package/docs/articles/custom-ci-cd-github.mdx +12 -3
  28. package/docs/articles/custom-ci-cd-gitlab.mdx +1 -1
  29. package/docs/articles/graphql.mdx +276 -0
  30. package/docs/articles/monetization/api-access.mdx +184 -0
  31. package/docs/articles/monetization/meters.mdx +4 -4
  32. package/docs/articles/monetization/monetization-policy.md +4 -1
  33. package/docs/articles/monetization/private-plans.md +3 -4
  34. package/docs/articles/monetization/stripe-integration.md +9 -0
  35. package/docs/articles/monetization/subscription-lifecycle.md +12 -11
  36. package/docs/articles/monorepo-deployment.mdx +37 -5
  37. package/docs/articles/opentelemetry.mdx +5 -2
  38. package/docs/articles/securing-the-gateway-with-client-mtls.mdx +68 -43
  39. package/docs/articles/step-1-setup-basic-gateway.mdx +1 -3
  40. package/docs/articles/step-2-add-rate-limiting.mdx +1 -1
  41. package/docs/articles/testing.mdx +1 -1
  42. package/docs/articles/troubleshooting.md +7 -3
  43. package/docs/articles/waf-ddos-akamai.md +35 -16
  44. package/docs/articles/waf-ddos-aws-waf-shield.mdx +35 -16
  45. package/docs/articles/waf-ddos-fastly.mdx +10 -7
  46. package/docs/cli/deploy.mdx +44 -9
  47. package/docs/cli/deploy.partial.mdx +44 -9
  48. package/docs/concepts/api-keys.md +2 -2
  49. package/docs/dev-portal/zudoku/components/callout.mdx +11 -18
  50. package/docs/dev-portal/zudoku/components/sidecar-box.mdx +131 -0
  51. package/docs/dev-portal/zudoku/configuration/api-catalog.md +62 -42
  52. package/docs/dev-portal/zudoku/configuration/api-reference.md +5 -4
  53. package/docs/dev-portal/zudoku/configuration/navigation.mdx +70 -7
  54. package/docs/dev-portal/zudoku/configuration/search.md +36 -0
  55. package/docs/dev-portal/zudoku/configuration/site.md +38 -0
  56. package/docs/dev-portal/zudoku/customization/colors-theme.mdx +51 -40
  57. package/docs/errors/rate-limit-exceeded.mdx +30 -3
  58. package/docs/guides/canary-routing-for-employees.mdx +103 -39
  59. package/docs/guides/modify-openapi-paths.mdx +3 -3
  60. package/docs/handlers/legacy-dev-portal-handler.mdx +1 -1
  61. package/docs/handlers/mcp-server.mdx +13 -11
  62. package/docs/handlers/url-forward.mdx +5 -1
  63. package/docs/handlers/url-rewrite.mdx +7 -2
  64. package/docs/handlers/websocket-handler.mdx +5 -1
  65. package/docs/mcp-gateway/observability/logging.mdx +19 -12
  66. package/docs/mcp-server/resources.mdx +27 -15
  67. package/docs/mcp-server/testing.mdx +0 -2
  68. package/docs/policies/_index.md +2 -0
  69. package/docs/policies/archive-request-azure-storage-inbound/doc.md +1 -1
  70. package/docs/policies/archive-response-azure-storage-outbound/doc.md +1 -1
  71. package/docs/policies/data-loss-prevention-inbound/doc.md +116 -0
  72. package/docs/policies/data-loss-prevention-inbound/intro.md +15 -0
  73. package/docs/policies/data-loss-prevention-inbound/schema.json +220 -0
  74. package/docs/policies/data-loss-prevention-outbound/doc.md +116 -0
  75. package/docs/policies/data-loss-prevention-outbound/intro.md +18 -0
  76. package/docs/policies/data-loss-prevention-outbound/schema.json +220 -0
  77. package/docs/policies/ip-restriction-inbound/policy.ts +1 -1
  78. package/docs/programmable-api/background-dispatcher.mdx +6 -8
  79. package/docs/programmable-api/http-problems.mdx +0 -18
  80. package/docs/programmable-api/jwt-service-plugin.mdx +131 -109
  81. package/docs/programmable-api/runtime-behaviors.mdx +4 -2
  82. package/docs/programmable-api/streaming-zone-cache.mdx +4 -6
  83. package/docs/programmable-api/web-crypto-apis.mdx +10 -6
  84. package/docs/programmable-api/zone-cache.mdx +1 -1
  85. package/docs/rate-limiting/combining-policies.mdx +293 -0
  86. package/docs/rate-limiting/dynamic-rate-limiting.mdx +240 -0
  87. package/docs/rate-limiting/getting-started.mdx +339 -0
  88. package/docs/rate-limiting/how-it-works.md +225 -0
  89. package/docs/rate-limiting/monitoring-and-troubleshooting.mdx +243 -0
  90. package/docs/{articles → rate-limiting}/per-user-rate-limits-using-db.mdx +39 -28
  91. package/package.json +4 -4
  92. package/docs/concepts/rate-limiting.md +0 -246
  93. package/docs/errors/get-head-body-error.mdx +0 -41
@@ -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