Sume 429 body: parse details.scope and window_seconds in TypeScript
A Sume rate_limited 429 names the read or write bucket and gives limit, window_seconds and retry_after_seconds. Parse them in TypeScript and wait.

A 429 rate_limited from the Sume API carries more than a wait time. The error envelope has a details object with limit, remaining (always 0 here), scope, window_seconds and retry_after_seconds. Read them, and your log line can say which budget was spent and how long to wait.
The fields
| Field | Value | Use |
|---|---|---|
| limit | Requests allowed in the window for that bucket | Compare with your plan row |
| remaining | 0 | Already spent |
| scope | read or write | Which budget refused you |
| window_seconds | 60 | Length of the fixed window |
| retry_after_seconds | 1 or more, rounded up | How long to wait |
Why scope is the useful one
Reads and writes have separate buckets. A 429 on read means your pollers are too tight, and shortening the submit rate will not help. A 429 on write means submits, cancellations or uploads are over the plan number. On a rate_limited response the retry-after header holds the same value as details.retry_after_seconds, so either works.
Typed parser
The function parses an error response and returns a wait in milliseconds, or null when the error is not a rate limit. It makes no network call, so it is easy to unit test.
type RateLimited = { scope: "read" | "write"; limit: number; windowSeconds: number; waitMs: number };
export function parseRateLimited(status: number, body: unknown, header?: string | null): RateLimited | null {
const err = (body as { error?: { code?: string; details?: Record<string, unknown> } })?.error;
if (status !== 429 || err?.code !== "rate_limited") return null;
const d = err.details ?? {};
const seconds = Number(d.retry_after_seconds ?? header ?? 1);
return {
scope: d.scope === "read" ? "read" : "write",
limit: Number(d.limit ?? 0),
windowSeconds: Number(d.window_seconds ?? 60),
waitMs: Math.max(1, seconds) * 1000,
};
}
const sample = { error: { code: "rate_limited", details: { limit: 120, remaining: 0, scope: "write", window_seconds: 60, retry_after_seconds: 17 } } };
console.log(parseRateLimited(429, sample)); // waitMs: 17000Do not confuse it
Other 429 codes use a different shape. queue_full means the workspace generation queue is full and rate_limit_unavailable means the limiter itself is degraded. The parser returns null for both, and the caller handles them separately. The code check is what keeps them apart.
Add some jitter
Many workers that sleep for the same retry_after_seconds wake together at the start of the next window. Add up to 20 percent random delay so they do not arrive as one burst.
Sources
Related posts
More in Developers
- Sume 429 retry with a deadline: give up when retry-after is too long
Retrying a Sume 429 forever blocks a request. Cap the total wait, give up when retry-after exceeds what is left, then surface the error. TypeScript wrapper.
- Sume API 503 codes: which ones are retryable and which are not
Four 503 responses look alike but differ in the retryable flag: deploy_draining, database_busy, provider_capacity_exceeded and any _not_configured code.
- Sume API 503 deploy_draining: retry after 5 seconds in Python
A redeploy answers 503 deploy_draining with retry-after. Why it is safe to retry, why a POST still needs an Idempotency-Key, and a stdlib Python retry loop.
- A blank Idempotency-Key on Sume is ignored, not rejected: guard it
An empty or whitespace Idempotency-Key header is treated as no key at all, so a retry can create a second job. Build a key that cannot be blank in Python.
Written by Sume