Rate limits
Per-endpoint request caps, and how to behave when you hit one.
Each endpoint has its own budget, measured over a rolling 60-second window.
| Endpoint | Limit |
|---|---|
POST /v1/commands/* | 60 per minute |
GET /v1/response_stream | 30 per minute |
GET /v1/attachments/* | 120 per minute |
POST /v1/handle-check | 30 per minute |
The stream limit counts connections, not events. Holding one stream open costs one request; a reconnect loop that fires every second will exhaust it.
Handle check is capped low on purpose — the underlying read is single-flight
per Mac, so concurrency there produces read_in_flight rather than throughput.
Exceeding a limit
HTTP/1.1 429 Too Many Requests
retry-after: 12{ "error": "rate_limited", "retryAfter": 12 }retryAfter is in seconds and is always at least 1. The Retry-After header
carries the same value.
async function withRetry(fn: () => Promise<Response>): Promise<Response> {
for (let attempt = 0; ; attempt++) {
const res = await fn();
if (res.status !== 429 || attempt >= 5) return res;
const wait = Number(res.headers.get("retry-after") ?? 1);
await new Promise((r) => setTimeout(r, wait * 1000));
}
}Unconfirmed
Limits are currently counted per client IP address and per server process, not per account. That has two consequences worth designing around: several clients behind one NAT share a budget, and the effective ceiling can vary as requests land on different processes. Per-account limiting is the intended design, so do not build anything that depends on the current behaviour.
Staying under the limit
- Hold one stream open rather than polling. The stream is the intended way to learn about outcomes, and it costs one connection.
- Batch nothing, pace everything. There is no bulk endpoint; 60 commands a minute is one per second sustained.
- Cache handle checks. Reachability changes rarely.
- Use conditional attachment requests. An
If-None-Matchhit returns304and is far cheaper than re-downloading.
If a limiter fails internally the request is allowed through rather than rejected, so an occasional burst above the stated cap is possible. Do not rely on it.