Skip to content

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

LimitValueScope
Sustained rate1000 requests / minutePer API key (or per user for JWT)
Burst100 requestsToken bucket refills at 1000/min toward a 100-token ceiling
Login attempts5 / 15 minPer email, POST /api/auth/login only
Manual camera trigger1 / 5 secPer 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):

HeaderExampleMeaning
X-RateLimit-Limit1000Requests per minute available on this bucket
X-RateLimit-Remaining947Tokens left in the current window
X-RateLimit-Reset1766332800Unix epoch seconds when the bucket refills to full
Retry-After12Seconds 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:

Terminal window
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
fi
done

TypeScript (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 r

Per-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:

OverrideRangeDefault
rate_limit_rpm10 – 100001000
rate_limit_burst10 – 1000100

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