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,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).
@@ -0,0 +1,225 @@
1
+ ---
2
+ title: How rate limiting works
3
+ sidebar_label: How it works
4
+ description:
5
+ Understand Zuplo's sliding window rate limiter — how requests are counted,
6
+ what each rateLimitBy mode does, the Complex Rate Limiting policy, and every
7
+ configuration option.
8
+ ---
9
+
10
+ This page covers the mechanics behind Zuplo's rate limiter: how requests are
11
+ counted, what each `rateLimitBy` mode does in detail, and every configuration
12
+ option available. If you just want to add a rate limit to your API, start with
13
+ the [Getting Started guide](./getting-started.mdx) instead — this page is the
14
+ deep dive you can read alongside or after it.
15
+
16
+ Zuplo's rate limiter uses a **sliding window algorithm** enforced globally
17
+ across all edge locations. Unlike a fixed window algorithm (which resets
18
+ counters at fixed intervals and can allow bursts at window boundaries), the
19
+ sliding window continuously tracks requests over a rolling time period. This
20
+ produces smoother, more predictable throttling behavior.
21
+
22
+ ## Key terms
23
+
24
+ A few terms show up repeatedly in the rate limiting docs. They are related but
25
+ not interchangeable.
26
+
27
+ - **Counter (or bucket)** — The running tally Zuplo keeps for a single caller
28
+ and a single policy. Each unique combination of policy `name` and caller
29
+ identifier gets its own counter. Two different policies tracking the same
30
+ caller do _not_ share a counter; two different callers under the same policy
31
+ do not share a counter either.
32
+ - **Rate limit key** — The string value that identifies a caller for bucketing.
33
+ For `rateLimitBy: "ip"` the key is the client's IP address; for `"user"` it is
34
+ `request.user.sub`; for `"function"` it is whatever your custom function
35
+ returns as `CustomRateLimitDetails.key`; for `"all"` there is a single
36
+ implicit key shared by every request to the route.
37
+ - **`identifier` option** — A field in the policy's configuration that points
38
+ Zuplo at your custom TypeScript function when `rateLimitBy` is `"function"`.
39
+ Zuplo calls that function on each request, and the function returns a
40
+ `CustomRateLimitDetails` object whose `key` property becomes the rate limit
41
+ key. In short: `identifier` is _where the function lives_; `key` is _what the
42
+ function returns_.
43
+
44
+ ## How `rateLimitBy` works
45
+
46
+ The `rateLimitBy` option determines how the rate limiter groups requests into
47
+ buckets. Both the standard
48
+ [Rate Limiting policy](../policies/rate-limit-inbound.mdx) and the
49
+ [Complex Rate Limiting policy](../policies/complex-rate-limit-inbound.mdx)
50
+ support the same four modes.
51
+
52
+ ### `ip`
53
+
54
+ Groups requests by the client's IP address. No authentication is required. This
55
+ is the simplest option and works well for public APIs or as a first layer of
56
+ protection.
57
+
58
+ :::caution
59
+
60
+ Multiple clients behind the same corporate proxy, cloud NAT, or shared Wi-Fi
61
+ network can share a single IP address. In these cases, IP-based rate limiting
62
+ can unfairly throttle unrelated users. For authenticated APIs, prefer
63
+ `rateLimitBy: "user"` instead.
64
+
65
+ :::
66
+
67
+ ### `user`
68
+
69
+ Groups requests by the authenticated user's identity (`request.user.sub`). When
70
+ using [API key authentication](../articles/api-key-authentication.mdx), the
71
+ `sub` value is the consumer name you assigned when creating the API key. When
72
+ using JWT authentication, it comes from the token's `sub` claim.
73
+
74
+ This is the recommended mode for authenticated APIs because it ties limits to
75
+ the actual consumer rather than a shared IP address.
76
+
77
+ :::note
78
+
79
+ The `user` mode requires an authentication policy (such as API key or JWT
80
+ authentication) earlier in the policy pipeline. If no authenticated user is
81
+ present on the request, the policy returns an error. See
82
+ [Getting Started §5](./getting-started.mdx#5-rate-limit-authenticated-users) for
83
+ a full authenticated pipeline example.
84
+
85
+ :::
86
+
87
+ ### `function`
88
+
89
+ Groups requests using a custom TypeScript function that you provide. The
90
+ function returns a `CustomRateLimitDetails` object containing a grouping key
91
+ and, optionally, overridden values for `requestsAllowed` and
92
+ `timeWindowMinutes`. See
93
+ [Custom rate limit functions](#custom-rate-limit-functions) below for the
94
+ function signature and field reference.
95
+
96
+ ### `all`
97
+
98
+ Applies a single shared counter across all requests to the route, regardless of
99
+ who makes them. Use this for global rate limits on endpoints that call
100
+ resource-constrained backends.
101
+
102
+ ## Custom rate limit functions
103
+
104
+ When `rateLimitBy` is set to `"function"`, Zuplo calls a TypeScript function you
105
+ provide on every request. The function receives the request, context, and policy
106
+ name, and returns a `CustomRateLimitDetails` object describing how to count that
107
+ request.
108
+
109
+ ```ts
110
+ import {
111
+ CustomRateLimitDetails,
112
+ ZuploContext,
113
+ ZuploRequest,
114
+ } from "@zuplo/runtime";
115
+
116
+ export function rateLimit(
117
+ request: ZuploRequest,
118
+ context: ZuploContext,
119
+ policyName: string,
120
+ ): CustomRateLimitDetails | undefined {
121
+ return {
122
+ key: request.user.sub,
123
+ requestsAllowed: 100,
124
+ timeWindowMinutes: 1,
125
+ };
126
+ }
127
+ ```
128
+
129
+ ### `CustomRateLimitDetails`
130
+
131
+ - `key` (required) — The string used to group requests into rate limit buckets.
132
+ - `requestsAllowed` (optional) — Overrides the policy's `requestsAllowed` value
133
+ for this request.
134
+ - `timeWindowMinutes` (optional) — Overrides the policy's `timeWindowMinutes`
135
+ value for this request.
136
+
137
+ Returning `undefined` skips rate limiting for the request entirely — useful for
138
+ health checks or privileged callers. The function can also be `async` if you
139
+ need to await a database lookup or external service call.
140
+
141
+ Wire the function into the policy using the `identifier` option. The policy's
142
+ configured `requestsAllowed` and `timeWindowMinutes` serve as defaults; the
143
+ function can override them per request.
144
+
145
+ For concrete walkthroughs (tier-based, route-based, method-based,
146
+ database-backed, selective bypass), see
147
+ [Dynamic Rate Limiting](./dynamic-rate-limiting.mdx). For an advanced
148
+ database-backed example with caching, see
149
+ [Per-user rate limiting with a database](./per-user-rate-limits-using-db.mdx).
150
+
151
+ ## Additional options
152
+
153
+ Both rate limiting policies support the following additional options:
154
+
155
+ - `headerMode` — Set to `"retry-after"` (default) to include the `Retry-After`
156
+ header in 429 responses, or `"none"` to omit it. The `Retry-After` value is
157
+ returned as a number of seconds (delay-seconds format).
158
+ - `mode` — Set to `"strict"` (default) or `"async"`. In **strict** mode, the
159
+ request is held until the rate limit check completes — the backend is never
160
+ called if the limit is exceeded. This adds some latency to every request
161
+ because the check hits a globally distributed rate limit service. In **async**
162
+ mode, the request proceeds to the backend in parallel with the rate limit
163
+ check. This minimizes added latency but means some requests may get through
164
+ even after the limit is exceeded. Async mode is a good fit when low latency
165
+ matters more than exact enforcement.
166
+ - `throwOnFailure` — Controls behavior when the rate limit service is
167
+ unreachable. When set to `false` (default), requests are allowed through
168
+ (fail-open). When set to `true`, the policy returns an error to the client.
169
+ The fail-open default prevents a rate limit service outage from blocking all
170
+ traffic to your API.
171
+
172
+ ## Complex Rate Limiting policy
173
+
174
+ The [Complex Rate Limiting policy](../policies/complex-rate-limit-inbound.mdx)
175
+ supports **multiple named counters** in a single policy. Each counter tracks a
176
+ different resource or unit of work.
177
+
178
+ ```json
179
+ {
180
+ "name": "my-complex-rate-limit-policy",
181
+ "policyType": "complex-rate-limit-inbound",
182
+ "handler": {
183
+ "export": "ComplexRateLimitInboundPolicy",
184
+ "module": "$import(@zuplo/runtime)",
185
+ "options": {
186
+ "rateLimitBy": "user",
187
+ "timeWindowMinutes": 1,
188
+ "limits": {
189
+ "requests": 100,
190
+ "compute": 500
191
+ }
192
+ }
193
+ }
194
+ }
195
+ ```
196
+
197
+ Override counter increments programmatically per request with
198
+ `ComplexRateLimitInboundPolicy.setIncrements()`. This suits usage-based pricing,
199
+ where different endpoints consume different amounts of a resource (for example,
200
+ counting compute units or tokens instead of raw requests).
201
+
202
+ ## Related resources
203
+
204
+ **Go deeper on configuration:**
205
+
206
+ - [Rate Limiting policy reference](../policies/rate-limit-inbound.mdx) — Every
207
+ option for the standard policy.
208
+ - [Complex Rate Limiting policy reference](../policies/complex-rate-limit-inbound.mdx)
209
+ — Multi-counter limits for usage-based pricing (enterprise).
210
+
211
+ **Learn by example:**
212
+
213
+ - [Dynamic Rate Limiting](./dynamic-rate-limiting.mdx) — Tiered limits by
214
+ customer type.
215
+ - [Per-user rate limiting with a database](./per-user-rate-limits-using-db.mdx)
216
+ — Look up limits at request time using ZoneCache and a database.
217
+
218
+ **Combine with other policies:**
219
+
220
+ - [Combining Policies](./combining-policies.mdx) — Stack multiple rate limits,
221
+ and pair rate limiting with quotas or monetization.
222
+ - [Quota policy](../policies/quota-inbound.mdx) — Monthly or billing-period
223
+ usage caps.
224
+ - [Monetization policy](../articles/monetization/monetization-policy.md) —
225
+ Subscription-based access control and metering.