Skip to content
JamFrame
Docs menu

Rate limits

How many API requests you can make each minute, the headers that show where you stand, the extra limits on some endpoints, and how to wait and retry after a 429.

Rate limits keep JamFrame fast and fair for everyone. This page shows how many requests you can make, how to see how close you are, and what to do when you hit a limit.

Requests per minute

Every API request counts against your organization’s limit for the current minute. All the API keys of all your workspaces share it, so more keys don’t raise it.

The limit depends on your plan. See Pricing, or read it from the RateLimit-Policy header of any answer.

A minute starts with your first request. When it ends, the count starts again from zero.

Headers on every answer

Every answer to a request with a valid API key has two headers:

Header Example What it means
RateLimit limit=100, remaining=87, reset=42 Requests allowed in this minute, requests left, and seconds until the count starts again.
RateLimit-Policy 100;w=60 The limit, and the length of the window in seconds.

Read remaining, and slow down before it reaches zero.

The older X-RateLimit-* and RateLimit-Limit headers are not sent.

When you go over

You get 429 Too Many Requests with a Retry-After header, in seconds, and this body:

{
  "error": "RATE_LIMITED",
  "message": "Rate limit exceeded. Retry after 42 seconds.",
  "detail": { "retryAfter": 42 }
}

Nothing was done and nothing was charged. Wait Retry-After seconds, then send the request again.

Extra limits on some endpoints

These count per workspace, on top of the per-minute limit:

Endpoint Limit
Generate a template with AI (POST /v1/designs/generate) 30 per 10 minutes
Remove a background (POST /v1/images/remove-background) 120 per 10 minutes

On these two endpoints, the RateLimit headers show the endpoint’s own limit, not the per-minute one.

Background removal can also answer 429 RATE_LIMITED when its servers are busy. That answer has detail.retryAfter but no Retry-After header.

Signed URLs, dynamic URLs and forms are opened by anyone, without an API key. They have their own limits per minute, the same on every plan:

What Per visitor (IP address) Per link or form Per workspace
Requests to a signed or dynamic URL 120 1,200 —
New renders from signed and dynamic URLs 20 30 120
Requests to a form 120 — —
Renders from a form 10 30 120
Uploads to a form 20 (100 an hour) 30 (300 an hour) —

A file served from the cache doesn’t count as a new render. Over a limit, the link answers 429 RATE_LIMITED with Retry-After.

After 5 failed renders in 10 minutes, a link or form stops rendering for a while and answers 429 too. A link still serves files from its cache while it is paused.

A workflow’s incoming webhook URL takes up to 60 calls a minute.

Render credits and AI credits are not rate limits. They are what your plan includes for each billing period. See Credits.

Back off and retry

  • When you get a 429, wait the full Retry-After time. Don’t retry at once in a loop.
  • For 500, 502, 503 and 504, wait a little longer after each try: 1 second, then 2, then 4.
  • Stop after a few tries, and log the error.
  • For long renders, send them with "async": true and get the result by webhook instead of asking again and again.
async function callWithRetry(url, options, maxTries = 5) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch(url, options);
    const retryable = res.status === 429 || res.status >= 500;
    if (!retryable || attempt === maxTries) return res;

    // Prefer the server's hint, then detail.retryAfter, then 1 s, 2 s, 4 s…
    let wait = Number(res.headers.get("Retry-After"));
    if (!wait) {
      const body = await res.json().catch(() => ({}));
      wait = body.detail?.retryAfter ?? 2 ** (attempt - 1);
    }
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  }
}

const res = await callWithRetry("https://api.jamframe.com/v1/designs", {
  headers: { Authorization: `Bearer ${process.env.JAMFRAME_API_KEY}` },
});
import os
import time

import requests


def call_with_retry(method, url, max_tries=5, **kwargs):
    for attempt in range(1, max_tries + 1):
        res = requests.request(method, url, **kwargs)
        retryable = res.status_code == 429 or res.status_code >= 500
        if not retryable or attempt == max_tries:
            return res
        # Prefer the server's hint, then detail.retryAfter, then 1 s, 2 s, 4 s…
        wait = res.headers.get("Retry-After")
        if not wait:
            try:
                wait = res.json().get("detail", {}).get("retryAfter")
            except ValueError:
                wait = None
        time.sleep(float(wait or 2 ** (attempt - 1)))


res = call_with_retry(
    "GET",
    "https://api.jamframe.com/v1/designs",
    headers={"Authorization": f"Bearer {os.environ['JAMFRAME_API_KEY']}"},
)

A render that timed out on your side may still have worked. Before you retry a render, check the render log, or use async renders and keep the job id.

Last updated

Still stuck?

Email [email protected] and a real person will reply. Tell us the template you were using and what you expected to happen.