# Rate limiting
After a caller authenticates, the Worker counts their requests and rejects them with HTTP 429 if they go too fast. That stops runaway loops, misconfigured publishers, and casual abuse from taking the service down.
# What we enforce
| Who | What | Limit | How we identify them |
|---|---|---|---|
| Signed-in user | Inbox + self-notify (GET/POST/DELETE under /v2/notifications…, except cross-user publish) |
300 requests per 60 seconds | Cognito sub |
| Publisher | Cross-user publish (POST /v2/notifications) |
600 requests per 60 seconds | API key name, or M2M client_id, or publisher sub |
Numbers live in packages/api/wrangler.toml. Staging and production use the same Cloudflare rate-limit namespaces (1001 / 1002) on this account.
# What is not limited
/health,/docs,/openapi.json(public; no auth)- Calls that fail auth (401) — we never get as far as counting them
- Local
wrangler dev/ unit tests when the rate-limit binding is absent — requests are allowed so laptop work stays simple
# Legacy /api/Notification/*
Those old compatibility routes are not rate-limited today. Limits apply to the current V2 API (/v2/*) that the npm client and new apps use. Legacy V1 callers (/api/Notification/*) still go through auth; they just skip this counter. See V1 compatibility.
# When a caller is over the limit
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/problem+json
{
"type": "about:blank",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded. Retry after 60 seconds."
}
Wait (or honour Retry-After) and try again. @inovus-medical/notifications already retries 429 when retries are allowed — including publish only if dedupeKey is set.
# How the Worker decides
After JWT / API-key auth:
POST /v2/notifications→ publish limiter, key =clientId ?? sub- Any other authenticated
/v2/*request → user limiter, key =sub
Code: packages/api/src/auth/rate-limit.ts. Wired with app.use('/v2/*', rateLimitMiddleware()) in app.ts.
# Changing the numbers
- Edit
simple.limit/simple.periodinpackages/api/wrangler.toml(Cloudflare only allows period 10 or 60 seconds). - Redeploy staging/production.
- Update this page (and DESIGN.md if the design note still mentions the old figures).
# Related
- App integration — how apps should handle 429
- Publishing — always set
dedupeKeyso publish retries stay safe - Setup troubleshooting — 429 row
- Client README — retry behaviour