Rate limiting
Configure backend rate limiting (public read, session, global, auth, public API, uploads, and other route-specific limits) for OpenQuok.
Connect your agent today
Draft from chat, review in your calendar, and publish only what you approve.
Overview
The backend uses express-rate-limit. Route rules live in backend/middlewares/publicRouteRegistry.ts. Limits and auth skip rules use the same registry.
| Limiter | Scope | Key | Default (production) |
|---|---|---|---|
| publicRead | Public CMS GETs (/company/*, /blog-system/*, /listings/*, allowlisted GET /image/download) | Trusted client IP | 600 / hr |
| session | Authenticated API under API_PREFIX | JWT sub | 2000 / hr |
| global | Other anonymous routes | Trusted client IP | 120 / hr |
| auth | /auth (not /oauth/*) | IP | 50 / 15 min |
| oauth | /auth/oauth/* | IP | 20 / 5 min |
| publicApi | /public/* | opo_ token or IP | 30 / hr |
| mcp | /mcp | Bearer, path token, or IP | 120 / hr |
| upload | /media/upload*, /public/upload* | Token or IP | 20 / hr |
| feedback | POST /feedback | IP | 10 / hr |
| integrationConnect | OAuth connect under /integrations | IP | 30 / 15 min |
| oauthToken | POST /oauth/token | IP | 30 / 15 min |
| publicWrite | Listing stats, blog activity, conversion tracking | IP | 60 / hr |
Config: backend/middlewares/rateLimit.ts, backend/config/GlobalConfig.ts.
Note
Warning
Cache-Control headers to reduce origin load.Client IP
backend/middlewares/trustedClientIp.ts picks the IP for keys and 429 logs:- TRUST_CLOUDFLARE_HEADERS — use
CF-Connecting-IPinstead ofreq.ip(default true in production when NOT_SECURED is false). - VERIFY_CLOUDFLARE_IP_RANGE — use
CF-Connecting-IPonly whenreq.ipis a Cloudflare edge IP.
Redis store
When RATE_LIMIT_REDIS_ENABLED is true (production default), all limiters use Redis. They share REDIS_HOST and REDIS_PORT with cache and BullMQ. Set RATE_LIMIT_REDIS_PREFIX and optional RATE_LIMIT_REDIS_DB to tune keys. If Redis is off or down at startup, counters stay in memory per API instance.
CMS cache headers
Public CMS GETs (same routes as publicRead) get Cache-Control from backend/middlewares/publicCmsCacheHeaders.ts:
| Route kind | Default header |
|---|---|
| CMS JSON | public, max-age=60, stale-while-revalidate=300 |
| GET /blog-system/rss | public, max-age=86400 |
| Public images | public, max-age=3600, stale-while-revalidate=86400 |
Anonymous public HTML uses matching hints in web/src/lib/seo/publicCmsPageCache.ts. Tune with PUBLIC_CMS_CACHE_ENABLED and PUBLIC_CMS_CACHE_* (on by default when NOT_SECURED is false).
429 logs
Search logs for Rate limit exceeded. Fields: limiter, trustedClientIp, userId, path, method, windowMs, max.
Environment variables
Set RATE_LIMIT_ENABLED to false to disable all limiters.
| Group | Variables |
|---|---|
| Master switch | RATE_LIMIT_ENABLED |
| Client IP | TRUST_CLOUDFLARE_HEADERS, VERIFY_CLOUDFLARE_IP_RANGE |
| Public read | PUBLIC_READ_RATE_LIMIT_WINDOW_MS, PUBLIC_READ_RATE_LIMIT_MAX |
| Session | SESSION_RATE_LIMIT_WINDOW_MS, SESSION_RATE_LIMIT_MAX |
| Global | RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX |
| Auth / OAuth | AUTH_RATE_LIMIT_*, OAUTH_RATE_LIMIT_* |
| Public API / MCP | PUBLIC_API_RATE_LIMIT_*, MCP_RATE_LIMIT_* |
| Upload / feedback | UPLOAD_RATE_LIMIT_*, FEEDBACK_RATE_LIMIT_* |
| Integrations | INTEGRATION_CONNECT_RATE_LIMIT_*, OAUTH_TOKEN_RATE_LIMIT_* |
| Public writes | PUBLIC_WRITE_RATE_LIMIT_* |
| Redis store | RATE_LIMIT_REDIS_ENABLED, RATE_LIMIT_REDIS_PREFIX, RATE_LIMIT_REDIS_DB |
| CMS cache | PUBLIC_CMS_CACHE_ENABLED, PUBLIC_CMS_CACHE_MAX_AGE, PUBLIC_CMS_CACHE_STALE_WHILE_REVALIDATE, PUBLIC_CMS_RSS_CACHE_MAX_AGE, PUBLIC_CMS_IMAGE_CACHE_* |
Example (development)
Copy backend/.env.development.example to backend/.env.development.local and adjust:
RATE_LIMIT_ENABLED=true
# Global (session / dashboard API)
RATE_LIMIT_WINDOW_MS=3600000
RATE_LIMIT_MAX=60
# Auth
AUTH_RATE_LIMIT_WINDOW_MS=900000
AUTH_RATE_LIMIT_MAX=50
# OAuth (Google)
OAUTH_RATE_LIMIT_WINDOW_MS=300000
OAUTH_RATE_LIMIT_MAX=20
# Public API (per opo_ token)
PUBLIC_API_RATE_LIMIT_WINDOW_MS=3600000
PUBLIC_API_RATE_LIMIT_MAX=30
# Uploads
UPLOAD_RATE_LIMIT_WINDOW_MS=3600000
UPLOAD_RATE_LIMIT_MAX=20
# Feedback
FEEDBACK_RATE_LIMIT_WINDOW_MS=3600000
FEEDBACK_RATE_LIMIT_MAX=10
# Integration connect + OAuth token exchange
INTEGRATION_CONNECT_RATE_LIMIT_WINDOW_MS=900000
INTEGRATION_CONNECT_RATE_LIMIT_MAX=30
OAUTH_TOKEN_RATE_LIMIT_WINDOW_MS=900000
OAUTH_TOKEN_RATE_LIMIT_MAX=30
# Anonymous public writes
PUBLIC_WRITE_RATE_LIMIT_WINDOW_MS=3600000
PUBLIC_WRITE_RATE_LIMIT_MAX=60 Production deploy
- Deploy the API with NOT_SECURED set to false and Redis configured.
- Check startup logs for
store: rediswhen RATE_LIMIT_REDIS_ENABLED is on. - Watch
Rate limit exceededlogs. Raise PUBLIC_READ_RATE_LIMIT_MAX or SESSION_RATE_LIMIT_MAX for real traffic. Lower caps only when you confirm abuse. - If a public page returns 503, check for
limiter: ”publicRead”in API logs. ConfirmCF-Connecting-IPbehind Cloudflare andCache-Controlon CMS GETs.
Workers and the web app do not need rate-limit env vars. See Production deployment.