Rate limits
The API is rate-limited on two levels: a token bucket per API key for day-to-day
fairness, and a ceiling per team so no single team can monopolize the platform.
Every response tells you where you stand, and a 429 tells you exactly how long to
wait.
Two levels: per-key bucket and per-team ceiling
Section titled “Two levels: per-key bucket and per-team ceiling”- Per-key token bucket. Each API key gets its own bucket that refills continuously. This is the limit you’ll meet in normal use, and it keeps one noisy script from starving your other integrations.
- Per-team ceiling. A separate aggregate limit applies across all keys in a team. It exists for fairness and abuse resistance — minting more keys doesn’t buy more total throughput, because every key in the team draws against the same team ceiling.
Buckets are sized by action class, because a read costs far less than provisioning an environment. The class is determined by the HTTP method:
| Action class | Example limit | Covers |
|---|---|---|
read |
1000/min | GET requests — lists, gets, observability reads. |
write |
100/min | PATCH, PUT, and DELETE — updates and removals. |
create |
5/min | POST — creating resources and starting jobs. |
Response headers
Section titled “Response headers”Every response — not just 429s — carries your current standing, so you can throttle
proactively instead of waiting to be rejected:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
The bucket’s ceiling for the matched action class. |
X-RateLimit-Remaining |
Tokens left in the current window. |
X-RateLimit-Reset |
Unix timestamp when the bucket next refills to full. |
Retry-After |
On a 429 only — seconds to wait before retrying. |
HTTP/1.1 200 OKX-RateLimit-Limit: 100X-RateLimit-Remaining: 87X-RateLimit-Reset: 1782192000The 429 response
Section titled “The 429 response”Exceed a limit and you get 429 Too Many Requests with a Retry-After header and the
standard error envelope:
HTTP/1.1 429 Too Many RequestsRetry-After: 12X-RateLimit-Limit: 100X-RateLimit-Remaining: 0X-RateLimit-Reset: 1782192012Content-Type: application/json{ "error": { "type": "rate_limit", "code": "rate_limit.exceeded", "message": "rate limit exceeded for write actions; retry after 12s", "doc_url": "https://docs.managed.dev/errors/rate_limit/rate_limit.exceeded" }, "request_id": "req_01J9…"}Introspect your limits
Section titled “Introspect your limits”To see your current standing, read GET /v1/rate-limits. It reports every bucket —
limit, remaining tokens, and reset time — plus the team ceiling, and the peek itself
doesn’t consume a token:
curl https://api.managed.dev/v1/rate-limits \ -H "Authorization: Bearer mfk_live_…" \ -H "Forge-Version: 2026-06-23"{ "data": { "buckets": [ { "class": "read", "limit": 1000, "remaining": 1000, "reset": 1782192060 }, { "class": "write", "limit": 100, "remaining": 87, "reset": 1782192000 }, { "class": "create", "limit": 5, "remaining": 5, "reset": 1782192000 } ], "team_ceiling": { "limit": 5000, "remaining": 4870, "reset": 1782192000 } }, "request_id": "req_01J9…"}limits, err := client.RateLimits.Get(ctx)if err != nil { return err}for _, b := range limits.Buckets { fmt.Println(*b.Class, b.Remaining, "of", b.Limit)}fmt.Println("team ceiling:", limits.TeamCeiling.Remaining, "of", limits.TeamCeiling.Limit)mf ratelimitsThe example figures above are illustrative — read your real ceilings off this endpoint.
Backing off correctly
Section titled “Backing off correctly”When you hit a 429, retry — but politely:
- Honor
Retry-Afterfirst. It’s the authoritative wait. Sleep at least that long before retrying. - Use exponential backoff with jitter for repeated failures: wait
base * 2^attempt, plus a random fraction, so a fleet of clients doesn’t retry in lockstep and re-spike the limit (the thundering-herd problem). - Cap your retries, then surface the error. Don’t loop forever.
- Throttle proactively. Watch
X-RateLimit-Remainingand slow down as it approaches zero, rather than sprinting into a wall of429s.
async function withRetry<T>(fn: () => Promise<Response>, max = 5): Promise<Response> { for (let attempt = 0; ; attempt++) { const res = await fn(); if (res.status !== 429 || attempt >= max) return res; const retryAfter = Number(res.headers.get("Retry-After")) || 0; const backoff = Math.min(2 ** attempt, 30) + Math.random(); await new Promise((r) => setTimeout(r, Math.max(retryAfter, backoff) * 1000)); }}The Go SDK implements this for you by default: it retries 429s
and 5xxs with exponential backoff and jitter, honoring Retry-After, so you get
polite retries without writing the loop.