Rate Limits & Quotas
Chronary enforces two types of limits to ensure fair usage and platform stability: per-key rate limits and monthly quotas.
Per-key rate limit
Section titled “Per-key rate limit”Every API key is limited to 10 requests per second. Requests that exceed this limit receive a 429 response with a Retry-After header indicating how many seconds to wait.
{ "error": { "type": "rate_limit_error", "message": "Rate limit exceeded. Retry after 1 second.", "param": null, "request_id": "req_abc123" }}The Retry-After response header contains the number of seconds to wait before retrying.
Monthly quotas by plan
Section titled “Monthly quotas by plan”Monthly quotas reset automatically at the start of each billing cycle. Exceeding a quota returns a 429 response with "type": "quota_exceeded".
| Metric | Free | Pro | Scale |
|---|---|---|---|
| Agents | 5 | Unlimited | Unlimited |
| Calendars | 10 | Unlimited | Unlimited |
| iCal Subscriptions | 3 per agent | Unlimited | Unlimited |
| Events created | 5,000/mo | Unlimited | Unlimited |
| API Calls | 50,000/mo | Unlimited | Unlimited |
| Webhook Deliveries | 10,000/mo | Unlimited | Unlimited |
| Availability Queries | 10,000/mo | Unlimited | Unlimited |
Cross-agent availability limits
Section titled “Cross-agent availability limits”When querying availability across multiple agents (e.g., finding a common free slot), additional limits apply:
| Parameter | Free | Pro | Scale |
|---|---|---|---|
| Max agents per query | 5 | 20 | 50 |
| Max date range | 30 days | 90 days | 365 days |
Checking your usage
Section titled “Checking your usage”Call GET /v1/usage to see your current consumption against your plan limits:
curl https://api.chronary.ai/v1/usage \ -H "Authorization: Bearer chr_sk_live_your_key_here"Example response:
{ "plan": "free", "period_start": "2026-04-01T00:00:00Z", "period_end": "2026-05-01T00:00:00Z", "usage": { "agents": { "used": 3, "limit": 5 }, "calendars": { "used": 7, "limit": 10 }, "events": { "used": 1842, "limit": 5000 }, "api_calls": { "used": 12034, "limit": 50000 }, "webhook_deliveries": { "used": 956, "limit": 10000 }, "availability_queries": { "used": 304, "limit": 10000 } }}Retry strategy
Section titled “Retry strategy”When you receive a 429 response, implement exponential backoff with jitter to avoid thundering-herd problems.
async function fetchWithRetry(url, options, maxRetries = 5) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options);
if (response.status !== 429) { return response; }
// Use Retry-After header if present, otherwise calculate backoff const retryAfter = response.headers.get('Retry-After'); const baseDelay = retryAfter ? parseFloat(retryAfter) * 1000 : Math.min(1000 * Math.pow(2, attempt), 30000);
// Add jitter: random delay between 0% and 100% of base delay const jitter = Math.random() * baseDelay; const delay = baseDelay + jitter;
console.log(`Rate limited. Retrying in ${Math.round(delay)}ms (attempt ${attempt + 1}/${maxRetries})`); await new Promise((resolve) => setTimeout(resolve, delay)); }
throw new Error('Max retries exceeded');}import timeimport randomimport requests
def fetch_with_retry(url, headers, max_retries=5): for attempt in range(max_retries): response = requests.get(url, headers=headers)
if response.status_code != 429: return response
# Use Retry-After header if present, otherwise calculate backoff retry_after = response.headers.get("Retry-After") base_delay = ( float(retry_after) if retry_after else min(2 ** attempt, 30) )
# Add jitter: random delay between 0% and 100% of base delay jitter = random.random() * base_delay delay = base_delay + jitter
print(f"Rate limited. Retrying in {delay:.1f}s (attempt {attempt + 1}/{max_retries})") time.sleep(delay)
raise Exception("Max retries exceeded")Monthly reset
Section titled “Monthly reset”Quotas reset automatically at the start of each billing period. The reset is handled via KV TTL — no manual action is required. You can check your current period and usage at any time with GET /v1/usage.