Rate limiting
The SyntheticBrew Engine is deliberately rate-limit-free in-process. Every protected route — chat, agents, knowledge bases, knowledge graphs — is expected to sit behind a reverse proxy that enforces per-IP and per-tenant limits. The engine returns 200/4xx/5xx; the proxy returns 429.
This page captures the canonical configuration patterns for the three proxies most customers deploy:
- Caddy — the cloud-team default; built-in
rate_limitdirective. - nginx — the most common on-prem choice.
- Traefik — Kubernetes Ingress users.
Why edge, not engine
Section titled “Why edge, not engine”Rate limiting and authentication are concerns of the network perimeter, not the application. Putting them in-process couples the engine to a particular memory store (sliding-window, leaky-bucket), forces every customer to accept the same defaults, and bypasses the same enforcement when traffic enters via a private network.
The engine’s job is to be deterministic about what it does on a successful request. The proxy’s job is to decide which requests get through.
This decision is documented in code at internal/app/chat_routes.go:20-22. Applying the same pattern to Knowledge Graphs (introduced in 1.3.0) means: the engine accepts every authenticated request and you protect /api/v1/knowledge-graphs/{bundle}/import and friends at the edge.
The cloud deploy uses Caddy in front of the engine. The relevant block:
@kg_writes { method POST PUT DELETE path /api/v1/knowledge-graphs/*}
# 60 mutations per IP per minute is comfortable for a human admin# editing a bundle by hand. Bulk-apply via brewctl typically issues# 1 request per `kg apply`, so the limit rarely fires for GitOps flows.route @kg_writes { rate_limit { zone kg_writes_per_ip 60 key {client_ip} events 60 window 1m }}
# Bulk import has a much larger blast radius — separate, stricter zone.@kg_import { method POST path /api/v1/knowledge-graphs/*/import}route @kg_import { rate_limit { zone kg_import_per_ip 10 key {client_ip} events 10 window 1m }}
# Reads are read-only and may be polled by dashboards.@kg_reads { method GET path /api/v1/knowledge-graphs/*}route @kg_reads { rate_limit { zone kg_reads_per_ip 600 key {client_ip} events 600 window 1m }}Caddy’s rate_limit module requires the caddyserver/rate_limit plugin. Build a custom Caddy binary with xcaddy build --with github.com/mholt/caddy-ratelimit or use one of the cloud images that ship with it preinstalled.
# In http {} block:limit_req_zone $binary_remote_addr zone=kg_writes_per_ip:10m rate=60r/m;limit_req_zone $binary_remote_addr zone=kg_import_per_ip:10m rate=10r/m;limit_req_zone $binary_remote_addr zone=kg_reads_per_ip:10m rate=600r/m;
# In server {} block:location ~ ^/api/v1/knowledge-graphs/.+/import$ { limit_req zone=kg_import_per_ip burst=5 nodelay; proxy_pass http://syntheticbrew_engine;}
location ~ ^/api/v1/knowledge-graphs/.* { limit_req zone=kg_writes_per_ip burst=10 nodelay; proxy_pass http://syntheticbrew_engine; # GET requests will also pass through this — set a separate location with # `if ($request_method = "GET") { ... }` only if you need separate read # quotas. The simplest deploys use a single mid-range limit for the whole # /knowledge-graphs subtree.}Traefik
Section titled “Traefik”Add a RateLimit middleware in your IngressRoute:
apiVersion: traefik.io/v1alpha1kind: Middlewaremetadata: name: kg-rate-limitspec: rateLimit: average: 60 period: 1m burst: 30 sourceCriterion: ipStrategy: depth: 1 # the engine sits behind cloud LB; depth=1 strips its IP.
---apiVersion: traefik.io/v1alpha1kind: IngressRoutemetadata: name: syntheticbrew-enginespec: routes: - match: PathPrefix(`/api/v1/knowledge-graphs`) kind: Rule middlewares: - name: kg-rate-limit services: - name: syntheticbrew-engine port: 9555What about tenant-level limits?
Section titled “What about tenant-level limits?”The snippets above key on client IP. For multi-tenant cloud deployments, key on the tenant ID extracted from the JWT — every authenticated SyntheticBrew JWT carries tenant_id.
In Caddy, with a custom JWT-extraction module, use {http.auth.user.tenant_id} as the key. In nginx, use auth_jwt to bind the claim to a variable then reference it from limit_req_zone. In Traefik, write a middleware that reads the header and supplies it via the sourceCriterion.requestHeaderName field.
Verifying the limit is active
Section titled “Verifying the limit is active”Hit the endpoint quickly enough to trip the limit:
for i in $(seq 1 100); do curl -s -o /dev/null -w "%{http_code}\n" \ -H "Authorization: Bearer $TOKEN" \ -X POST "$EDGE/api/v1/knowledge-graphs/test/import" \ -d '{"version":"1.0.0","schemas":[],"entities":[]}'done | sort | uniq -cYou should see a mix of 4xx (validation / collision from the engine for the test bundle) and 429s from the proxy. If you see only engine responses, the rate-limit module is not in the request path — check the proxy logs.