Rate limits
NovaVMS rate-limits every authenticated request. The limiter is a token-bucket keyed by API key (for service accounts) or by user id (for JWT callers). Exceeding the bucket returns 429 TOO_MANY_REQUESTS with a Retry-After header. Limits are enforced since v1.0.
Default limits
| Limit | Value | Scope |
|---|---|---|
| Sustained rate | 1000 requests / minute | Per API key (or per user for JWT) |
| Burst | 100 requests | Token bucket refills at 1000/min toward a 100-token ceiling |
| Login attempts | 5 / 15 min | Per email, POST /api/auth/login only |
| Manual camera trigger | 1 / 5 sec | Per camera, POST /api/v1/cameras/{id}/trigger only |
Writes (POST, PATCH, DELETE) count the same as reads. Health checks (GET /healthz) are exempt.
Response headers
Every response — not just rate-limited ones — includes these headers (since v1.0):
| Header | Example | Meaning |
|---|---|---|
X-RateLimit-Limit | 1000 | Requests per minute available on this bucket |
X-RateLimit-Remaining | 947 | Tokens left in the current window |
X-RateLimit-Reset | 1766332800 | Unix epoch seconds when the bucket refills to full |
Retry-After | 12 | Seconds to wait; only on 429 responses |
429 response
{ "error": { "code": "RATE_LIMITED", "message": "rate limit exceeded, retry after 12s" }}Always honour Retry-After. The value is the minimum wait; adding jitter (±20%) prevents thundering herd when many workers share a key.
Handling 429 in code
Bash with jq:
while :; do resp=$(curl -sS -o /tmp/body -w '%{http_code}' \ -H "Authorization: Bearer sk_live_abc123" \ https://novavms.novalien.com/api/v1/cameras) if [ "$resp" = "429" ]; then wait=$(curl -sI -H "Authorization: Bearer sk_live_abc123" \ https://novavms.novalien.com/api/v1/cameras | awk '/^Retry-After/{print $2}' | tr -d '\r') sleep "${wait:-5}" else cat /tmp/body break fidoneTypeScript (manual — the SDK handles this automatically):
// @novavms/sdk >= 1.0.0 retries on 429 up to 3 times using Retry-After.// Raw-fetch example for comparison:async function call(url: string, token: string, retries = 3): Promise<Response> { const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } }); if (res.status === 429 && retries > 0) { const wait = Number(res.headers.get('Retry-After') ?? 5) * 1000; await new Promise((r) => setTimeout(r, wait + Math.random() * 1000)); return call(url, token, retries - 1); } return res;}Python:
# novavms >= 1.0.0 retries on 429 automatically (max_retries=3).# httpx example:import httpx, time, random
def call(url: str, token: str, retries: int = 3) -> httpx.Response: r = httpx.get(url, headers={"Authorization": f"Bearer {token}"}) if r.status_code == 429 and retries > 0: wait = int(r.headers.get("Retry-After", "5")) + random.random() time.sleep(wait) return call(url, token, retries - 1) return rPer-key overrides
The default bucket is shared across all keys in an org. To isolate one integration from another (for example, a bursty exporter should not starve a real-time alert bridge), an admin can set a per-key override from /admin/service-accounts. Supported overrides:
| Override | Range | Default |
|---|---|---|
rate_limit_rpm | 10 – 10000 | 1000 |
rate_limit_burst | 10 – 1000 | 100 |
Override requests go through Novalien support if you need more than 10000 rpm — sustained traffic above that usually indicates a polling loop that should be a webhook.
Quota is per key, not per IP
NovaVMS does not limit by source IP. The bucket follows the credential, so rotating outbound IPs, running from Lambda, or NAT’ing through a load balancer all behave the same. This is intentional — IP-based limits interact badly with mobile carriers and CGNAT (the same reason refresh tokens are not IP-bound, see D-R2).
Related
- Rate-limit handling — tracing a 429 back to the noisy caller
- Webhook setup — switch from polling to push to cut RPM
- API keys — per-key quota overrides