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.
- package/docs/ai-gateway/getting-started.mdx +2 -1
- package/docs/ai-gateway/integrations/ai-sdk.mdx +17 -0
- package/docs/ai-gateway/introduction.mdx +5 -5
- package/docs/ai-gateway/providers.mdx +2 -0
- package/docs/analytics/access-and-entitlements.md +71 -0
- package/docs/analytics/overview.md +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/ci-cd-github/basic-deployment.mdx +10 -1
- package/docs/articles/ci-cd-github/deploy-and-test.mdx +14 -1
- package/docs/articles/ci-cd-github/local-testing.mdx +3 -1
- package/docs/articles/ci-cd-github/pr-preview-environments.mdx +36 -4
- package/docs/articles/custom-ci-cd-github.mdx +11 -2
- package/docs/articles/monetization/api-access.mdx +184 -0
- package/docs/articles/monetization/meters.mdx +4 -4
- package/docs/articles/monetization/monetization-policy.md +4 -1
- package/docs/articles/monetization/private-plans.md +3 -4
- package/docs/articles/monetization/stripe-integration.md +9 -0
- package/docs/articles/monetization/subscription-lifecycle.md +12 -11
- package/docs/articles/monorepo-deployment.mdx +20 -2
- package/docs/cli/deploy.mdx +32 -0
- package/docs/cli/deploy.partial.mdx +32 -0
- package/docs/concepts/api-keys.md +2 -2
- package/docs/dev-portal/zudoku/components/callout.mdx +11 -18
- 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/policies/_index.md +2 -0
- 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/programmable-api/background-dispatcher.mdx +6 -8
- 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 -27
- package/package.json +4 -4
- package/docs/concepts/rate-limiting.md +0 -246
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
The Data Loss Prevention (DLP) policy scans upstream response bodies for
|
|
2
|
+
sensitive data — personally identifiable information (PII), secrets and API
|
|
3
|
+
keys for dozens of vendors, payment and bank identifiers, and national IDs for
|
|
4
|
+
many countries — using a catalog of 60+ built-in recognizers plus any custom
|
|
5
|
+
patterns you add. When a match is found it takes a configurable action: mask
|
|
6
|
+
the matches, block the response, or log a warning and let it through.
|
|
7
|
+
|
|
8
|
+
Recognizers are selected individually or via entity groups (`secret`,
|
|
9
|
+
`finance`, `pii`, `id-us`, `id-uk`, `region-eu`, …). Detection runs entirely in the
|
|
10
|
+
gateway isolate using regular expressions, checksums (Luhn, mod-97, Verhoeff,
|
|
11
|
+
and friends), and context-word scoring — no response data leaves the gateway.
|
|
12
|
+
This is especially useful in front of APIs that interface with user-generated
|
|
13
|
+
content, MCP servers, and AI consumers, where a response might otherwise leak
|
|
14
|
+
data the client should never see.
|
|
15
|
+
|
|
16
|
+
Pair with the
|
|
17
|
+
[Data Loss Prevention - Inbound](/docs/policies/data-loss-prevention-inbound)
|
|
18
|
+
policy to also scan incoming requests before they reach your handler.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "https://cdn.zuplo.com/policies/runtime/schemas/data-loss-prevention-outbound.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"title": "Data Loss Prevention",
|
|
6
|
+
"isDeprecated": false,
|
|
7
|
+
"isPaidAddOn": false,
|
|
8
|
+
"isEnterprise": false,
|
|
9
|
+
"isInternal": false,
|
|
10
|
+
"isBeta": false,
|
|
11
|
+
"isHidden": false,
|
|
12
|
+
"requiresAI": false,
|
|
13
|
+
"products": ["api-gateway"],
|
|
14
|
+
"description": "Scans the upstream response body for sensitive data — PII, secrets, and financial identifiers — using an extensible catalog of built-in recognizers plus any custom patterns, and takes a configurable action when a match is found.\n\nThe action is one of `mask` (redact matches before returning the response), `block` (replace the response with a `422` listing the detected entity names only), or `log` (record a warning and return unchanged). Only text content types are inspected; binary bodies pass through untouched, and the body is read from a clone so the client still receives the original stream.",
|
|
15
|
+
"deprecatedMessage": "",
|
|
16
|
+
"required": ["handler"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"handler": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"default": {},
|
|
21
|
+
"required": ["export", "module", "options"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"export": {
|
|
24
|
+
"const": "DataLossPreventionOutboundPolicy",
|
|
25
|
+
"description": "The name of the exported type"
|
|
26
|
+
},
|
|
27
|
+
"module": {
|
|
28
|
+
"const": "$import(@zuplo/runtime)",
|
|
29
|
+
"description": "The module containing the policy"
|
|
30
|
+
},
|
|
31
|
+
"options": {
|
|
32
|
+
"title": "DataLossPreventionOutboundPolicyOptions",
|
|
33
|
+
"type": "object",
|
|
34
|
+
"description": "The options for the Data Loss Prevention outbound policy. Scans the upstream response body for sensitive data and applies the configured action (mask, block, or log).",
|
|
35
|
+
"additionalProperties": false,
|
|
36
|
+
"required": [],
|
|
37
|
+
"examples": [
|
|
38
|
+
{
|
|
39
|
+
"action": "mask",
|
|
40
|
+
"entities": ["secret", "finance", "contact-email", "id-us-ssn"],
|
|
41
|
+
"mask": "[REDACTED]"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"properties": {
|
|
45
|
+
"engine": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"enum": ["builtin"],
|
|
48
|
+
"default": "builtin",
|
|
49
|
+
"description": "The detection engine. Only `builtin` (in-isolate regex + checksum detection with context-word scoring) is available today. This is the extension point for a future hosted `presidio-service` mode; declaring it now keeps adding that mode an additive, non-breaking change."
|
|
50
|
+
},
|
|
51
|
+
"entities": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"description": "Built-in recognizer ids and/or group selectors to enable. Entity ids follow a {category}-{scope}-{name} taxonomy, and any dash-aligned id prefix acts as a selector (for example `secret` is every secret, `id-au` is Australia's identifiers, `secret-aws` is both AWS entities), plus the named groups `pii` and `region-eu`. Available selectors: `contact`, `finance`, `finance-us`, `id`, `id-au`, `id-br`, `id-ca`, `id-es`, `id-fr`, `id-in`, `id-it`, `id-nl`, `id-pl`, `id-sg`, `id-uk`, `id-us`, `network`, `pii`, `region-eu`, `secret`, `secret-aws`. When omitted, the full built-in catalog is used.",
|
|
54
|
+
"items": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"enum": [
|
|
57
|
+
"contact",
|
|
58
|
+
"finance",
|
|
59
|
+
"finance-us",
|
|
60
|
+
"id",
|
|
61
|
+
"id-au",
|
|
62
|
+
"id-br",
|
|
63
|
+
"id-ca",
|
|
64
|
+
"id-es",
|
|
65
|
+
"id-fr",
|
|
66
|
+
"id-in",
|
|
67
|
+
"id-it",
|
|
68
|
+
"id-nl",
|
|
69
|
+
"id-pl",
|
|
70
|
+
"id-sg",
|
|
71
|
+
"id-uk",
|
|
72
|
+
"id-us",
|
|
73
|
+
"network",
|
|
74
|
+
"pii",
|
|
75
|
+
"region-eu",
|
|
76
|
+
"secret",
|
|
77
|
+
"secret-aws",
|
|
78
|
+
"contact-email",
|
|
79
|
+
"contact-phone",
|
|
80
|
+
"finance-credit-card",
|
|
81
|
+
"finance-crypto-wallet",
|
|
82
|
+
"finance-cvv",
|
|
83
|
+
"finance-iban",
|
|
84
|
+
"finance-swift-bic",
|
|
85
|
+
"finance-us-aba-routing",
|
|
86
|
+
"finance-us-bank-account",
|
|
87
|
+
"id-au-abn",
|
|
88
|
+
"id-au-acn",
|
|
89
|
+
"id-au-medicare",
|
|
90
|
+
"id-au-tfn",
|
|
91
|
+
"id-br-cpf",
|
|
92
|
+
"id-ca-sin",
|
|
93
|
+
"id-es-nif",
|
|
94
|
+
"id-fr-nir",
|
|
95
|
+
"id-in-aadhaar",
|
|
96
|
+
"id-in-pan",
|
|
97
|
+
"id-it-fiscal-code",
|
|
98
|
+
"id-nl-bsn",
|
|
99
|
+
"id-pl-pesel",
|
|
100
|
+
"id-sg-nric",
|
|
101
|
+
"id-uk-nhs",
|
|
102
|
+
"id-uk-nino",
|
|
103
|
+
"id-us-itin",
|
|
104
|
+
"id-us-passport",
|
|
105
|
+
"id-us-ssn",
|
|
106
|
+
"network-ipv4",
|
|
107
|
+
"network-ipv6",
|
|
108
|
+
"network-mac",
|
|
109
|
+
"secret-anthropic",
|
|
110
|
+
"secret-aws-access-key",
|
|
111
|
+
"secret-aws-bedrock",
|
|
112
|
+
"secret-azure-client",
|
|
113
|
+
"secret-databricks",
|
|
114
|
+
"secret-digitalocean",
|
|
115
|
+
"secret-discord-webhook",
|
|
116
|
+
"secret-github",
|
|
117
|
+
"secret-gitlab",
|
|
118
|
+
"secret-google-api-key",
|
|
119
|
+
"secret-heroku",
|
|
120
|
+
"secret-hugging-face",
|
|
121
|
+
"secret-jwt",
|
|
122
|
+
"secret-mailchimp",
|
|
123
|
+
"secret-mailgun",
|
|
124
|
+
"secret-npm",
|
|
125
|
+
"secret-openai",
|
|
126
|
+
"secret-perplexity",
|
|
127
|
+
"secret-postman",
|
|
128
|
+
"secret-private-key",
|
|
129
|
+
"secret-pypi",
|
|
130
|
+
"secret-sendgrid",
|
|
131
|
+
"secret-sentry",
|
|
132
|
+
"secret-shopify",
|
|
133
|
+
"secret-slack",
|
|
134
|
+
"secret-square",
|
|
135
|
+
"secret-stripe",
|
|
136
|
+
"secret-telegram-bot",
|
|
137
|
+
"secret-terraform",
|
|
138
|
+
"secret-twilio",
|
|
139
|
+
"secret-zuplo"
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"customPatterns": {
|
|
144
|
+
"type": "array",
|
|
145
|
+
"description": "Additional customer-defined regex recognizers. Invalid patterns are logged and skipped rather than failing the response.",
|
|
146
|
+
"items": {
|
|
147
|
+
"type": "object",
|
|
148
|
+
"title": "DlpCustomPattern",
|
|
149
|
+
"additionalProperties": false,
|
|
150
|
+
"required": ["name", "pattern"],
|
|
151
|
+
"properties": {
|
|
152
|
+
"name": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"description": "Identifier reported in findings and block details for this pattern."
|
|
155
|
+
},
|
|
156
|
+
"pattern": {
|
|
157
|
+
"type": "string",
|
|
158
|
+
"description": "A JavaScript regular expression source string. Remember to escape backslashes for JSON (for example `\\\\d` for a digit)."
|
|
159
|
+
},
|
|
160
|
+
"confidence": {
|
|
161
|
+
"type": "number",
|
|
162
|
+
"minimum": 0,
|
|
163
|
+
"maximum": 1,
|
|
164
|
+
"default": 0.85,
|
|
165
|
+
"description": "Base confidence (0-1) for matches of this pattern. The default of 0.85 is above the default detection threshold; combine a low value with `context` words for patterns that are only sensitive in context."
|
|
166
|
+
},
|
|
167
|
+
"context": {
|
|
168
|
+
"type": "array",
|
|
169
|
+
"items": {
|
|
170
|
+
"type": "string"
|
|
171
|
+
},
|
|
172
|
+
"description": "Context words that boost a match's confidence by 0.45 when one appears near the match (in the surrounding field, label, or key)."
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
"action": {
|
|
178
|
+
"type": "string",
|
|
179
|
+
"enum": ["mask", "block", "log"],
|
|
180
|
+
"default": "mask",
|
|
181
|
+
"description": "What to do when sensitive data is detected. `mask` redacts matches before returning the response, `block` replaces the response with a 422 listing only the detected entity names, and `log` records a warning and returns the response unchanged."
|
|
182
|
+
},
|
|
183
|
+
"mask": {
|
|
184
|
+
"type": "string",
|
|
185
|
+
"default": "[REDACTED]",
|
|
186
|
+
"examples": ["[REDACTED]"],
|
|
187
|
+
"description": "The string that replaces detected values when `action` is `mask`."
|
|
188
|
+
},
|
|
189
|
+
"minConfidence": {
|
|
190
|
+
"type": "number",
|
|
191
|
+
"minimum": 0,
|
|
192
|
+
"maximum": 1,
|
|
193
|
+
"default": 0.5,
|
|
194
|
+
"x-show-example": false,
|
|
195
|
+
"description": "Minimum confidence (0-1) a match must reach to count as a finding. Context-dependent recognizers (for example `finance-us-bank-account` or `finance-us-aba-routing`) sit below the default threshold of 0.5 until a context word near the match boosts them above it. Lower the threshold to surface them everywhere; raise it to keep only prefix- or checksum-validated matches."
|
|
196
|
+
},
|
|
197
|
+
"contentTypes": {
|
|
198
|
+
"type": "array",
|
|
199
|
+
"description": "Override the set of scannable content-type prefixes. When omitted, the built-in text content-type allow-list (JSON, XML, form-encoded, text/\\*) is used.",
|
|
200
|
+
"items": {
|
|
201
|
+
"type": "string"
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
"examples": [
|
|
208
|
+
{
|
|
209
|
+
"export": "DataLossPreventionOutboundPolicy",
|
|
210
|
+
"module": "$import(@zuplo/runtime)",
|
|
211
|
+
"options": {
|
|
212
|
+
"action": "mask",
|
|
213
|
+
"entities": ["secret", "finance", "contact-email", "id-us-ssn"],
|
|
214
|
+
"mask": "[REDACTED]"
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -13,7 +13,7 @@ groups entries and invokes your callback with batches at regular intervals.
|
|
|
13
13
|
```ts
|
|
14
14
|
new BackgroundDispatcher<T>(
|
|
15
15
|
dispatchFunction: (entries: T[]) => Promise<void>,
|
|
16
|
-
options: { msDelay: number }
|
|
16
|
+
options: { msDelay: number; name?: string }
|
|
17
17
|
)
|
|
18
18
|
```
|
|
19
19
|
|
|
@@ -21,6 +21,7 @@ Creates a new background dispatcher instance.
|
|
|
21
21
|
|
|
22
22
|
- `dispatchFunction` - Asynchronous function called with batched entries
|
|
23
23
|
- `options.msDelay` - Milliseconds between dispatch calls (required, non-zero)
|
|
24
|
+
- `options.name` - Optional name that identifies the dispatcher in error logs
|
|
24
25
|
- `T` - The type of entries being batched
|
|
25
26
|
|
|
26
27
|
## Methods
|
|
@@ -30,7 +31,7 @@ Creates a new background dispatcher instance.
|
|
|
30
31
|
Adds an entry to the batch queue for later dispatch.
|
|
31
32
|
|
|
32
33
|
```ts
|
|
33
|
-
enqueue(entry: T
|
|
34
|
+
enqueue(entry: T): void
|
|
34
35
|
```
|
|
35
36
|
|
|
36
37
|
## Example
|
|
@@ -74,12 +75,9 @@ const backgroundDispatcher = new BackgroundDispatcher<ExampleEntry>(
|
|
|
74
75
|
// This is an example Request Handler that used the component, a simple
|
|
75
76
|
// "Hello World" handler.
|
|
76
77
|
export default async function (request: ZuploRequest, context: ZuploContext) {
|
|
77
|
-
backgroundDispatcher.enqueue(
|
|
78
|
-
{
|
|
79
|
-
|
|
80
|
-
},
|
|
81
|
-
context,
|
|
82
|
-
);
|
|
78
|
+
backgroundDispatcher.enqueue({
|
|
79
|
+
message: `new request on '${request.url}' with id ${context.requestId}`,
|
|
80
|
+
});
|
|
83
81
|
|
|
84
82
|
return "Hello World!";
|
|
85
83
|
}
|
|
@@ -18,7 +18,7 @@ time-to-live (TTL) after which it expires and is removed from the cache. Each
|
|
|
18
18
|
cached object can be up to 512 MB in size.
|
|
19
19
|
|
|
20
20
|
There's an demonstration of ZoneCache use in the
|
|
21
|
-
[Per User Rate Limits Using a Database](../
|
|
21
|
+
[Per User Rate Limits Using a Database](../rate-limiting/per-user-rate-limits-using-db.mdx)
|
|
22
22
|
example.
|
|
23
23
|
|
|
24
24
|
## Constructor
|
|
@@ -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)
|