Rate Limits
C.O.S. API v1
Limits at a glance
| Traffic type | Window | Limit | Scope |
|---|---|---|---|
| Authenticated | 60 seconds | 60 requests | Per organisation |
Unauthenticated (/health, /version) |
60 seconds | 60 requests | Global (all callers) |
How it works
C.O.S. uses a fixed-window counter backed by Redis. Each window is a UTC
calendar minute (:00 to :59).
-
Authenticated — A counter keyed on your
org_idincrements on every request. Once it hits 60 the API returns429for the rest of that minute. The counter resets at the start of the next minute. -
Unauthenticated — A single global counter across all callers increments on every request to
/v1/healthand/v1/version. The global limit protects the service from unauthenticated flood traffic.
Fail-open. If Redis becomes unavailable, the counter check is skipped and
requests are allowed through rather than returning 429 responses. A WARNING
is logged server-side. During a Redis outage, rate limits are temporarily
unenforced — callers will not receive 429 errors even if they exceed the
normal threshold.
Fixed-window burst note
Because the window is calendar-aligned rather than rolling, up to 2× the stated limit can be served in a short period spanning a window boundary. For example: 60 requests in the last second of minute N, and 60 requests in the first second of minute N+1, are each counted as 60 in their own windows and are both allowed. This is an accepted characteristic of the current implementation.
Response headers
Every response includes:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Your limit for this window |
X-RateLimit-Remaining |
Requests remaining in the current window |
X-RateLimit-Reset |
UTC epoch seconds when the current window ends |
On a 429 response only:
| Header | Description |
|---|---|
Retry-After |
Seconds until the next window starts |
429 response body
Rate limit errors use RFC 7807 Problem Details
with content type application/problem+json:
{
"type": "https://cosprotocol.io/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "Organisation rate limit exceeded. Retry after 1 second.",
"cos_error_code": "RATE_LIMIT_EXCEEDED",
"retry_after": 1,
"instance": "/v1/organisations/7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
The retry_after field duplicates the Retry-After header value.
Retry guidance
When you receive a 429:
- Read the
Retry-Afterheader (orretry_afterin the body). - Wait that many seconds before retrying.
- Do not retry immediately — requests without waiting will continue to be rejected for the rest of the window.
Python:
import time
import requests
def get_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries):
resp = requests.get(url, headers=headers)
if resp.status_code != 429:
return resp
retry_after = int(resp.headers.get("Retry-After", 60))
if attempt < max_retries - 1:
time.sleep(retry_after)
return resp # return final 429 to caller
curl:
curl --retry 3 --retry-delay 2 \
-H "Authorization: Bearer $COS_API_KEY" \
https://cosprotocol.io/v1/organisations/$ORG_ID
Staying within the limit
At 60 requests per minute you have ~1 request per second sustained. If your integration iterates large result sets (e.g. all members across many organisations), add a short sleep between pages:
import time, requests
cursor = None
while True:
params = {"cursor": cursor} if cursor else {}
resp = requests.get(
f"https://cosprotocol.io/v1/organisations/{ORG_ID}/members",
headers={"Authorization": f"Bearer {API_KEY}"},
params=params,
)
data = resp.json()
for member in data["data"]:
process(member)
cursor = data["meta"].get("next_cursor")
if not cursor:
break
time.sleep(1) # one page per second stays within 60/min
Roadmap
BUG-005 tracks a planned improvement to add per-IP limiting for unauthenticated endpoints (Phase 60 hard deadline). When shipped, the global counter will be complemented by a per-source-IP counter. The limit values above will not decrease.