Rate Limits
Lathe Studio enforces rate limits to ensure fair usage and API stability.
Tier Limits#
| Tier | Monthly Limit | Notes |
|---|---|---|
| Basic | No API access | API requires Pro or Enterprise |
| Pro | 1,000 calls/month | Resets on billing cycle anniversary |
| Enterprise | 10,000 calls/month | Custom limits available — contact sales |
Monthly limits reset at the start of each billing cycle (the anniversary of your subscription start date).
Per-Minute Limit#
In addition to the monthly limit, a per-minute in-memory limit applies to prevent burst abuse. This limit is not published and is generous enough that well-behaved CI pipelines will never hit it.
If you encounter 429 errors from the per-minute limit (rather than the monthly limit), the Retry-After header tells you how many seconds to wait.
Rate Limit Headers#
Every API response includes these headers:
| Header | Description |
|---|---|
X-RateLimit-Remaining | Calls remaining in the current billing period |
X-RateLimit-Reset | Unix timestamp (seconds) when the monthly limit resets |
Monitor X-RateLimit-Remaining in your integrations to avoid unexpected 429 errors.
429 Too Many Requests#
When you exceed the rate limit, the API returns a 429 response:
{
"error": "rate_limit_exceeded",
"message": "You have exceeded your monthly API call limit. Upgrade to Enterprise for 10,000 calls/month.",
"details": {
"limit": 1000,
"reset_at": "2026-04-01T00:00:00Z"
}
}
Retry Guidance#
Use exponential backoff with jitter when retrying after a 429:
wait = min(base * 2^attempt + random_jitter, max_wait)
Recommended values:
| Parameter | Value |
|---|---|
| Base wait | 1 second |
| Max wait | 60 seconds |
| Jitter | 0–500ms random |
| Max attempts | 5 |
Example in Python:
import time
import random
import httpx
def create_build_with_retry(payload, api_key, max_attempts=5):
for attempt in range(max_attempts):
response = httpx.post(
"https://app.lathe.studio/api/v1/builds",
headers={"Authorization": f"Bearer {api_key}"},
json=payload,
)
if response.status_code == 429:
wait = min(2 ** attempt + random.uniform(0, 0.5), 60)
time.sleep(wait)
continue
response.raise_for_status()
return response.json()
raise RuntimeError("Max retry attempts exceeded")
Reducing API Usage#
If you're approaching your monthly limit:
- Cache read responses —
GET /buildsandGET /projectsresponses are stable. Cache them for 60 seconds. - Batch operations — The
POST /testruns/{id}/resultsendpoint accepts an array of results in a single call. - Use webhooks instead of polling — Subscribe to
testrun.completed.v1instead of pollingGET /testruns/{id}in a loop. - Upgrade your plan — Move to Enterprise for 10,000 calls/month.