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.
- package/docs/ai-gateway/getting-started.mdx +14 -9
- package/docs/ai-gateway/integrations/ai-sdk.mdx +17 -0
- package/docs/ai-gateway/introduction.mdx +12 -10
- package/docs/ai-gateway/providers.mdx +2 -0
- package/docs/analytics/access-and-entitlements.md +71 -0
- package/docs/analytics/overview.md +63 -0
- package/docs/analytics/reference/metrics-glossary.md +105 -0
- package/docs/analytics/reference/url-parameters.md +66 -0
- package/docs/analytics/shared-controls.md +121 -0
- package/docs/analytics/tabs/agents.md +88 -0
- package/docs/analytics/tabs/consumers.md +73 -0
- package/docs/analytics/tabs/graphql.md +77 -0
- package/docs/analytics/tabs/mcp.md +80 -0
- package/docs/analytics/tabs/origins.md +82 -0
- package/docs/analytics/tabs/requests.md +96 -0
- package/docs/articles/api-key-buckets.mdx +4 -2
- package/docs/articles/archiving-requests-to-storage.mdx +4 -4
- package/docs/articles/branch-based-deployments.mdx +10 -8
- package/docs/articles/ci-cd-github/basic-deployment.mdx +10 -1
- package/docs/articles/ci-cd-github/cleanup-on-branch-delete.mdx +52 -31
- 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 +53 -10
- package/docs/articles/custom-ci-cd-azure.mdx +1 -1
- package/docs/articles/custom-ci-cd-bitbucket.mdx +1 -1
- package/docs/articles/custom-ci-cd-circleci.mdx +1 -1
- package/docs/articles/custom-ci-cd-github.mdx +12 -3
- package/docs/articles/custom-ci-cd-gitlab.mdx +1 -1
- package/docs/articles/graphql.mdx +276 -0
- 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 +37 -5
- package/docs/articles/opentelemetry.mdx +5 -2
- package/docs/articles/securing-the-gateway-with-client-mtls.mdx +68 -43
- package/docs/articles/step-1-setup-basic-gateway.mdx +1 -3
- package/docs/articles/step-2-add-rate-limiting.mdx +1 -1
- package/docs/articles/testing.mdx +1 -1
- package/docs/articles/troubleshooting.md +7 -3
- package/docs/articles/waf-ddos-akamai.md +35 -16
- package/docs/articles/waf-ddos-aws-waf-shield.mdx +35 -16
- package/docs/articles/waf-ddos-fastly.mdx +10 -7
- package/docs/cli/deploy.mdx +44 -9
- package/docs/cli/deploy.partial.mdx +44 -9
- package/docs/concepts/api-keys.md +2 -2
- package/docs/dev-portal/zudoku/components/callout.mdx +11 -18
- package/docs/dev-portal/zudoku/components/sidecar-box.mdx +131 -0
- package/docs/dev-portal/zudoku/configuration/api-catalog.md +62 -42
- package/docs/dev-portal/zudoku/configuration/api-reference.md +5 -4
- package/docs/dev-portal/zudoku/configuration/navigation.mdx +70 -7
- 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/guides/canary-routing-for-employees.mdx +103 -39
- package/docs/guides/modify-openapi-paths.mdx +3 -3
- package/docs/handlers/legacy-dev-portal-handler.mdx +1 -1
- package/docs/handlers/mcp-server.mdx +13 -11
- package/docs/handlers/url-forward.mdx +5 -1
- package/docs/handlers/url-rewrite.mdx +7 -2
- package/docs/handlers/websocket-handler.mdx +5 -1
- package/docs/mcp-gateway/observability/logging.mdx +19 -12
- package/docs/mcp-server/resources.mdx +27 -15
- package/docs/mcp-server/testing.mdx +0 -2
- package/docs/policies/_index.md +2 -0
- package/docs/policies/archive-request-azure-storage-inbound/doc.md +1 -1
- package/docs/policies/archive-response-azure-storage-outbound/doc.md +1 -1
- package/docs/policies/data-loss-prevention-inbound/doc.md +116 -0
- package/docs/policies/data-loss-prevention-inbound/intro.md +15 -0
- package/docs/policies/data-loss-prevention-inbound/schema.json +220 -0
- package/docs/policies/data-loss-prevention-outbound/doc.md +116 -0
- package/docs/policies/data-loss-prevention-outbound/intro.md +18 -0
- package/docs/policies/data-loss-prevention-outbound/schema.json +220 -0
- package/docs/policies/ip-restriction-inbound/policy.ts +1 -1
- package/docs/programmable-api/background-dispatcher.mdx +6 -8
- package/docs/programmable-api/http-problems.mdx +0 -18
- package/docs/programmable-api/jwt-service-plugin.mdx +131 -109
- package/docs/programmable-api/runtime-behaviors.mdx +4 -2
- package/docs/programmable-api/streaming-zone-cache.mdx +4 -6
- package/docs/programmable-api/web-crypto-apis.mdx +10 -6
- package/docs/programmable-api/zone-cache.mdx +1 -1
- package/docs/rate-limiting/combining-policies.mdx +293 -0
- package/docs/rate-limiting/dynamic-rate-limiting.mdx +240 -0
- package/docs/rate-limiting/getting-started.mdx +339 -0
- package/docs/rate-limiting/how-it-works.md +225 -0
- package/docs/rate-limiting/monitoring-and-troubleshooting.mdx +243 -0
- package/docs/{articles → rate-limiting}/per-user-rate-limits-using-db.mdx +39 -28
- package/package.json +4 -4
- package/docs/concepts/rate-limiting.md +0 -246
- 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.
|