Rate limiting
The one round trip a limited key costs.
packages/api/src/ratelimit/ratelimit.controller.ts by packages/docs/tools/generate-reference.mjs. Do not edit by hand: src/reference.spec.ts regenerates it and fails on a difference.POST /v1/ratelimit/check -- one of the two endpoints in the product that
are called once per customer request rather than once per vendor action.
Three consequences, and they are the same three the ingest endpoint has:
- It writes no audit row and takes no advisory lock. 0003's per- workspace audit chain serialises writers, and serialising the customer's request path behind their own dashboard is the 22.7 s stall the week 2 review reproduced. A rate-limit check is traffic, not a control-plane change.
- It touches Postgres only to authenticate, and even that is cached for
hotAuthCacheMs(seeHotSecretKeyGuard, which states what that costs). The counters are Redis, per report section 3.3. - It answers 200 for a denial. The 429 belongs to the customer's caller and is issued by the SDK in the customer's process, with the customer's headers. This endpoint reports a verdict; it is not the thing being rate-limited.
POST /v1/ratelimit/check
Answers 200 on success.
Authentication.
- A
krsk_secret key only, resolved once and reused forKEYRING_HOT_AUTH_CACHE_MS(5 s by default).
Body. Validated by this schema, from the control plane's own source:
What the SDK asks, and it is deliberately the SDK that carries the limits.
The limits are the vendor's own policy about the vendor's own tenants, and they reached the SDK from us, on the policy feed, resolved against the project default (migration 0008). Sending them back means this endpoint reads no Postgres at all: it is Redis and nothing else, which is the entire point of report section 3.3's placement argument. A vendor whose node sends a larger limit than we issued has only relaxed a bound on their own customers, which they could equally do by not calling us -- so this is not an escalation. It is bounded anyway, by the same numbers migration 0008 checks, because an unbounded window count is an unbounded number of Redis keys.
project_id, key_id and tenant_id are strings an SDK sent. They are
never trusted as authorisation: the workspace comes from the authenticated
principal, and it is the first component of every Redis key, so a forged
subject id can only ever collide with the sender's own counters.
const RateLimitCheck = z.object({
project_id: z.uuid(),
env: Env,
key_id: z.uuid().nullable().default(null),
tenant_id: z.uuid().nullable().default(null),
/**
* How much of the window this request spends. Zero is a *peek*: it reports
* the headers without charging, which is what an adapter needs when the
* request was already refused for another reason.
*/
cost: z.number().int().min(0).max(1_000).default(1),
limits: RateLimits.min(1),
});Shared validators
Defined once in packages/api/src/validation.ts and used by the schemas above.
const Env = z.enum(['live', 'test']);The same bounds migration 0008's keyring.rate_limits_valid checks, so a bad
set is a 400 with a field path rather than a 500 wrapping a constraint
violation. The database keeps the check regardless: this API is not the only
thing that will ever write that column, and a limit is an authorisation
bound.
window_ms has a floor of one second because these counters are one network
hop away (report section 3.2): a 100 ms window enforced across an RTT is a
number we cannot honestly claim to hold. The ceiling is 31 days, which is the
longest period anyone means by "per month".
const RateLimits = z
.array(
z.object({
id: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/),
limit: z.number().int().min(1).max(1_000_000_000),
window_ms: z.number().int().min(1_000).max(2_678_400_000),
algorithm: z.enum(['sliding', 'fixed']),
scope: z.enum(['key', 'tenant']),
}),
)
.max(8)
.refine(
(rules) => new Set(rules.map((rule) => rule.id)).size === rules.length,
// Two limits of one name share a counter and disagree about its bound, and
// `RateLimit-Policy` is a structured-field dictionary that cannot express
// the duplicate at all.
{ message: 'rate limit ids must be unique' },
);