Rate limits
/track and /engage are rate limited to keep the API healthy for everyone. Limits are enforced at two levels: per IP address and per project. Exceeding either returns 429.
Limits
| Limit | Default | Keyed by |
|---|---|---|
| Per-IP rate limit | 100 requests / 10 seconds | Client IP address |
| Per-project rate limit | 1,000 requests / 10 seconds | Project token |
Whichever limit is hit first returns the 429. Both apply to /track and /engage.
429 response
When a request is rate limited, the whole batch is rejected - nothing in it is inserted - so it is always safe to retry the exact same payload. The response includes a Retry-After header (seconds, as an integer of at least 1):
bashcurl -i -X POST https://cohorly-service.velloalabs.com/track \-H "Content-Type: application/json" \-H "X-Cohorly-Token: YOUR_PROJECT_TOKEN" \-d '{ "event": "Signed Up", "properties": { "distinct_id": "user_123" } }'HTTP/1.1 429 Too Many RequestsRetry-After: 8Content-Type: application/json{ "status": 0, "error": "rate limited" }
Monthly event quota
Separately from the rate limit, each plan has a monthly event quota. If a batch would push your project's account over its quota for the current calendar month, the whole batch is rejected with 429 and a fixed Retry-After: 3600:
json{ "status": 0, "error": "monthly event quota exceeded" }
Check your plan's event allowance in your account's usage page. As with rate limiting, the batch is rejected atomically, so it is safe to retry later.
Recommended client behavior
On any 429, back off exponentially and retry rather than dropping the batch:
- Respect the
Retry-Afterheader when present - wait at least that many seconds before retrying. - Otherwise, use exponential backoff (for example starting at 2 seconds, doubling on each attempt, capped at a reasonable maximum) with a little random jitter to avoid retry storms.
- Keep queuing new events locally while backing off - a rate limit is transient, not a rejection of your data.