spicyapiDocs
Main content

Rate limits

How the bucket works, what draws from it, and why concurrency is a separate question.

One bucket per account

The open API surface (/api/v1/*) is rate-limited per account, not per key — per key, anyone could route around it by creating more keys.

The default allowance is 20 requests per 10 seconds.

It is a token bucket rather than a fixed-window counter: capacity 20, refilled over 10 seconds (roughly 2 tokens per second). Steady state is 2 requests per second with a burst of 20 available. Fixed windows let through double the burst at the boundary — fill the window at the end, fill the next one at the start — whereas a token bucket smooths that out.

Every /api/v1 endpoint draws from the same bucket

createTask, retry, recordInfo, chat/credit, models, common/upload-url and common/download-url all take tokens from one place.

The harder you poll, the fewer tasks you can submit. Worst case, polling starves the bucket and createTask starts returning 429. This is the concrete reason we keep recommending webhooks over polling.

The model catalog draws on this bucket too. Fetching it before every task creation gives away half your quota for nothing — cache it in your process.

upload-url carries an additional allowance of 60 requests per minute per account, on top of the bucket above. Signing an upload ticket is free local computation for us but a write authorisation that no longer passes through us — the two need separate ceilings.

Every bucket

Limits are mounted per surface, so everything under one surface shares a middleware chain. As an API consumer you can only ever hit the first two:

BucketCoversCounted byDefault allowance
open_apievery endpoint under /api/v1/* (including the models catalog)account20 per 10 seconds
upload_url/api/v1/common/upload-urlaccount60 per minute (on top of open_api)
console surfaceauthenticated /console/v1/* endpointsaccount120 per minute
catalogthe public, anonymous catalog behind the marketing site (not /api/v1/models)source IP300 per minute

Per account, not per key

open_api counts against the account. Minting more keys buys you nothing — if the limit were per key, working around it would be one click in the console.

upload_url is a genuinely separate bucket, though: uploading a reference image draws a token from both, and either one running dry returns 429.

These thresholds are hot-reloadable operational settings; the numbers above are the defaults. Trust the X-RateLimit-Limit header, and do not hard-code the defaults into your client.

Response headers

HeaderPresent onMeaning
X-RateLimit-LimitEvery responseAllowance for the current window
X-RateLimit-RemainingEvery responseTokens left
Retry-After429 onlySeconds to wait, minimum 1

X-RateLimit-Remaining is sent on successful responses too, so you can slow down before hitting a 429 rather than after.

What a limit looks like

{
  "code": 429,
  "msg": "请求过于频繁,请稍后重试",
  "request_id": "req_…"
}

HTTP status 429, with Retry-After.

Over-limit requests are rejected, not queued

They return immediately. Queuing them would just let latency pile up and hand you a connection that never returns — harder to handle than a clear 429.

Handling it

Read Retry-After. Do not hammer at a fixed interval.

JavaScript
async function callRespectingLimits(url, init) {
  for (let i = 0; i < 5; i++) {
    const res = await fetch(url, init);

    if (res.status !== 429) return res;

    // The server already told you how long to wait; don't guess
    const wait = Number(res.headers.get('retry-after') ?? 1);
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
  throw new Error('persistently rate-limited');
}
Python
import time, requests


def call_respecting_limits(method: str, url: str, **kwargs) -> requests.Response:
    for _ in range(5):
        res = requests.request(method, url, **kwargs)

        if res.status_code != 429:
            return res

        # The server already told you how long to wait; don't guess
        time.sleep(float(res.headers.get("retry-after", 1)))

    raise RuntimeError("persistently rate-limited")

Concurrency is a separate question

The limit governs submission rate, not how many tasks are running

Once a task is accepted into the queue it stops consuming rate-limit budget. A hundred tasks generating at once will not produce a 429.

What actually bounds tasks in flight, none of which is the rate limit:

  • Available balance — every in-flight task holds funds. When the balance runs out, new tasks get 40201.
  • Spend caps — key daily, key lifetime, platform daily. Any of them hitting the ceiling gives 40202.
  • Upstream capacity — when every route for a model is down you get 50301.

So the answer to "I want more tasks running at once" is usually to top up or raise a spend cap, not to raise the rate limit.

Asking for more

After a period of stable usage you can request a higher allowance in the console. Describing your traffic shape — peak QPS, whether you run batches, how large a batch gets — makes the review considerably faster.

Until then, a client-side concurrency gate usually helps more than a higher limit. Cap in-flight requests at a fixed number (5, say) and combine it with Retry-After backoff; that runs comfortably within the default allowance.

When the limiter itself is unavailable

Rate limiting depends on an external counter. If it fails, the configured posture decides what happens: allow (keep serving) or reject (protect the allowance). Reject is the default — "we could not read the configuration" must never come out as "there is no limit".

When configured to reject you receive 429 with Retry-After: 1, not 500. That is deliberate: 500 means "we are broken", and client SDKs overwhelmingly retry it immediately, adding load during an incident. 429 with Retry-After means "back off and come back", which is what is needed.

So do not read 429 as proof that you were going too fast. Honouring Retry-After is the correct response either way.

On this page