Skip to content

Rate limits

NovaVMS rate-limits requests at two layers:

  1. Per-service-account — a token-bucket keyed on the SA identity, refilled at the configured per-minute rate. Implemented by middleware.SARateLimit(perMinute) in cloud/internal/middleware/ratelimit.go.
  2. Per-camera (manual trigger only) — at most one POST /api/v1/cameras/{id}/trigger per camera per 5 seconds, enforced in cloud/internal/devices/camera_trigger.go:130.

There is no per-user (JWT) rate-limit middleware in v1. A separate per-IP RateLimit middleware exists in code but is not wired into the main authenticated route group as of 2026-05-22.

What gets limited

BucketRateBurstScopeSource
Service-account API key600 / minute600 (token bucket starts full)Per service_account.idmain.go calls SARateLimit(600)
Camera manual trigger1 / 5 sec1Per cameracamera_trigger.go:130

Writes (POST, PATCH, PUT, DELETE) count the same as reads. Health checks at /api/health, /api/v1/health, and /api/v1/health/ready are unauthenticated and are not subject to either limiter.

What 429 looks like

{
"error": {
"code": "RATE_LIMITED",
"message": "too many requests"
}
}

There is no Retry-After header in v1 (the apierror writer does not emit one). There is also no X-RateLimit-Limit / Remaining / Reset header. Treat 429 as “back off and retry”; do not parse headers for the wait time.

Handling 429 in code

Use exponential backoff with jitter. Start at 1 s, double up to 30 s, cap retries at 5.

Bash:

Terminal window
attempt=0; max=5; backoff=1
until [ $attempt -ge $max ]; do
resp=$(curl -sS -o /tmp/body -w '%{http_code}' \
-H "Authorization: Bearer $NOVAVMS_KEY" \
"https://novavms.novalien.com/api/v1/cameras")
if [ "$resp" = "429" ]; then
sleep $(( backoff + RANDOM % 2 ))
backoff=$(( backoff * 2 ))
attempt=$(( attempt + 1 ))
else
cat /tmp/body
break
fi
done

TypeScript:

// @novavms/sdk >= 1.0.0 retries on 429 with exponential backoff up to 5 times.
// Raw-fetch example:
async function call(url: string, token: string, attempt = 0): Promise<Response> {
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
if (res.status === 429 && attempt < 5) {
const wait = (2 ** attempt) * 1000 + Math.random() * 500;
await new Promise((r) => setTimeout(r, wait));
return call(url, token, attempt + 1);
}
return res;
}

Python:

# novavms >= 1.0.0 retries on 429 automatically (max_retries=5).
import httpx, time, random
def call(url: str, token: str, attempt: int = 0) -> httpx.Response:
r = httpx.get(url, headers={"Authorization": f"Bearer {token}"})
if r.status_code == 429 and attempt < 5:
wait = (2 ** attempt) + random.random()
time.sleep(wait)
return call(url, token, attempt + 1)
return r

What’s NOT in v1

These were described in earlier doc drafts but are not implemented as of 2026-05-22:

  • X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset response headers.
  • Retry-After on 429 responses.
  • Per-SA quota overrides via admin UI (rate_limit_rpm / rate_limit_burst).
  • Per-email login attempt lockout — the per-IP RateLimit middleware exists but is not applied to the auth route group. (ACCOUNT_LOCKED 423 still fires on 5 failed logins per the accounts service, but the lockout policy is in service code, not in the rate-limit middleware.)

If you need any of the above, file a feature request — the bucket implementation in middleware.SARateLimit is straightforward to extend.

Quota follows the credential

NovaVMS does not limit by source IP for SA callers. The bucket follows the credential, so rotating outbound IPs, running from Lambda, or NAT’ing through a load balancer all behave the same. JWT callers are not bucketed at all in v1 (the implementation lives in SARateLimit only — JWT requests pass through without bucketing).