Skip to content

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_limit directive.
  • nginx — the most common on-prem choice.
  • Traefik — Kubernetes Ingress users.

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.
}

Add a RateLimit middleware in your IngressRoute:

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: kg-rate-limit
spec:
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/v1alpha1
kind: IngressRoute
metadata:
name: syntheticbrew-engine
spec:
routes:
- match: PathPrefix(`/api/v1/knowledge-graphs`)
kind: Rule
middlewares:
- name: kg-rate-limit
services:
- name: syntheticbrew-engine
port: 9555

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.

Hit the endpoint quickly enough to trip the limit:

Terminal window
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 -c

You 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.