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.
Files changed (52) hide show
  1. package/docs/ai-gateway/getting-started.mdx +2 -1
  2. package/docs/ai-gateway/integrations/ai-sdk.mdx +17 -0
  3. package/docs/ai-gateway/introduction.mdx +5 -5
  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/ci-cd-github/basic-deployment.mdx +10 -1
  17. package/docs/articles/ci-cd-github/deploy-and-test.mdx +14 -1
  18. package/docs/articles/ci-cd-github/local-testing.mdx +3 -1
  19. package/docs/articles/ci-cd-github/pr-preview-environments.mdx +36 -4
  20. package/docs/articles/custom-ci-cd-github.mdx +11 -2
  21. package/docs/articles/monetization/api-access.mdx +184 -0
  22. package/docs/articles/monetization/meters.mdx +4 -4
  23. package/docs/articles/monetization/monetization-policy.md +4 -1
  24. package/docs/articles/monetization/private-plans.md +3 -4
  25. package/docs/articles/monetization/stripe-integration.md +9 -0
  26. package/docs/articles/monetization/subscription-lifecycle.md +12 -11
  27. package/docs/articles/monorepo-deployment.mdx +20 -2
  28. package/docs/cli/deploy.mdx +32 -0
  29. package/docs/cli/deploy.partial.mdx +32 -0
  30. package/docs/concepts/api-keys.md +2 -2
  31. package/docs/dev-portal/zudoku/components/callout.mdx +11 -18
  32. package/docs/dev-portal/zudoku/configuration/search.md +36 -0
  33. package/docs/dev-portal/zudoku/configuration/site.md +38 -0
  34. package/docs/dev-portal/zudoku/customization/colors-theme.mdx +51 -40
  35. package/docs/errors/rate-limit-exceeded.mdx +30 -3
  36. package/docs/policies/_index.md +2 -0
  37. package/docs/policies/data-loss-prevention-inbound/doc.md +116 -0
  38. package/docs/policies/data-loss-prevention-inbound/intro.md +15 -0
  39. package/docs/policies/data-loss-prevention-inbound/schema.json +220 -0
  40. package/docs/policies/data-loss-prevention-outbound/doc.md +116 -0
  41. package/docs/policies/data-loss-prevention-outbound/intro.md +18 -0
  42. package/docs/policies/data-loss-prevention-outbound/schema.json +220 -0
  43. package/docs/programmable-api/background-dispatcher.mdx +6 -8
  44. package/docs/programmable-api/zone-cache.mdx +1 -1
  45. package/docs/rate-limiting/combining-policies.mdx +293 -0
  46. package/docs/rate-limiting/dynamic-rate-limiting.mdx +240 -0
  47. package/docs/rate-limiting/getting-started.mdx +339 -0
  48. package/docs/rate-limiting/how-it-works.md +225 -0
  49. package/docs/rate-limiting/monitoring-and-troubleshooting.mdx +243 -0
  50. package/docs/{articles → rate-limiting}/per-user-rate-limits-using-db.mdx +39 -27
  51. package/package.json +4 -4
  52. 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).