Appearance
Rate limits
All Krafter APIs are rate-limited. Authenticated routes are keyed per API key team; public unauthenticated routes are keyed per client IP. Each route belongs to one of four buckets, and counters in different buckets are independent — exceeding one bucket does not affect another for the same caller.
Buckets
| Bucket | Limit | Applied to |
|---|---|---|
default | 100 req / 60s | Every authenticated /api/v1/... endpoint via the :api_authenticated pipeline. |
strict | 30 req / 60s | Expensive endpoints — flag evaluate / bootstrap / track, push subscriber registration, audit scans run, public surveys. |
public | 200 req / 60s | Public unauthenticated endpoints — form submissions, public analytics ingest, webhook ingest. |
log_ingest | 300 req / 60s | Log ingest endpoints, keyed per project ingest token. |
Some endpoints pipe through both default and strict. In that case each bucket counts independently — the request will fail when either counter exceeds its limit. The response headers (x-ratelimit-*) only reflect the last bucket evaluated (strict overwrites default), so a request near the default ceiling can still succeed and report the remaining strict budget without surfacing how close it is to the default cap. Per-endpoint docs include a "Rate limit" callout when a non-default bucket is in play.
Response headers
Every API response includes the bucket counters in lowercase headers:
x-ratelimit-limit— the bucket's configured limit (e.g.100)x-ratelimit-remaining— requests left in the current windowx-ratelimit-reset— Unix timestamp (seconds) when the window resets
Exceeding the limit (429)
When a client exceeds its bucket, the response is:
- HTTP
429 Too Many Requests retry-after: <seconds>header (RFC 9110)- JSON body:
{"error": "Rate limit exceeded", "retry_after": <seconds>}
http
HTTP/1.1 429 Too Many Requests
content-type: application/json
retry-after: 23
x-ratelimit-limit: 100
x-ratelimit-remaining: 0
x-ratelimit-reset: 1714128360
{"error": "Rate limit exceeded", "retry_after": 23}Audit API exception
The Audit API normally wraps responses in the {data, meta, error} envelope, but its :strict_rate_limit 429 responses use the platform shape above (no envelope). See Audit overview → Rate-limit responses for details.
Window semantics
Windows are fixed-window: counters reset on a clock boundary derived from (unix_timestamp / window_seconds) * window_seconds. A client that hits the bucket at second 59 of a 60-second window will see counters reset 1 second later, not 60 seconds later.
Because reset is wall-clock aligned (not sliding), a burst at the end of one window followed by a burst at the start of the next can temporarily exceed the per-minute average — by design, to keep the limiter cheap.
Disabling for tests
The rate limiter checks Application.get_env(:krafter, :rate_limit_enabled, true) — when the value is false, the plug is a no-op. Production has it enabled; the test environment disables it so test suites are not throttled.