Image API error 429: rate limits, retries, and webhooks
A 429 response means the API rejected a request for now. How to read it, stay within the limits, retry after a pause, and use webhooks instead of polling.
Rendergrid editorsAbout the editors
In brief
A 429 response means the API is rejecting one request for now and asking you to wait. It is not a task status: an accepted task ends as completed or failed. Three limits apply to everyday requests: 120 requests a minute for the account, 100 tasks queued or in progress at once, and one poll per task every 5 seconds. Keep your own queue under those ceilings, retry after the pause the response asks for with the same X-Idempotency-Key, and use a webhook instead of polling.
What does a 429 response mean?
429 is the HTTP status for too many requests. The API sends it when it rejects a request for now, and the response says how long to wait. The error code and the details fields tell the main situations apart:
- Too many requests. More than 120 in a 60-second window, for the whole account:
ERR_RATE_LIMIT. - Too many tasks. 100 are already queued or in progress:
ERR_QUOTA_EXCEEDED. - Polling too soon. The same task was polled again within 5 seconds:
ERR_RATE_LIMITwithdetails.min_interval_seconds. - The same request twice at once. A retry with the same key arrived while the first request was still being accepted:
details.reasonisidempotency_in_flight. - A block after failed sign-ins. 30
401responses, or 30403responses, in a minute from one IP address:ERR_RATE_LIMITwithdetails.reasonset toburst_block, for up to 120 seconds.
Every 429 carries a Retry-After header in seconds. The first two responses look like this; the numbers are examples, so read the real values from each response.
HTTP/1.1 429 Too Many Requests Retry-After: 17 { "error": { "code": "ERR_RATE_LIMIT", "message": "rate limit exceeded", "details": { "limit": 120, "window": "1m", "retry_after": 17 } } }
HTTP/1.1 429 Too Many Requests Retry-After: 2 { "error": { "code": "ERR_QUOTA_EXCEEDED", "message": "concurrency limit reached", "details": { "limit": 100, "scope": "account" } } }
An accepted request that finishes ends differently, with an image like this cover: five wooden cubes, from one request.
The prompt behind this image
Arrange five identical wooden cubes in a straight horizontal row on light gray. Keep equal gaps and aligned front faces. Photograph from slightly above the objects under soft light from the left. Show every cube completely with orderly shadows and wide margins. No lettering, logos, people, or watermarks.
Nano Banana 2.1
nano-banana-2.1 · 1K · 1:1 · $0.02
Nano Banana 2.1, 1K, 1:1, one attempt. Five wooden cubes stand in a straight row across the middle of the frame on light gray, with a lot of empty space above and below and a narrower margin at each end. Their top edges line up, and each casts a soft shadow to its right, which fits the prompt’s light from the left. Departures from the prompt: the view is straight down, so each cube shows mostly its top face, not the slightly raised view the prompt asked for; the cubes are not identical, since each has its own grain and the two outer ones look a little wider than the three in the middle, and the gaps are close to equal rather than exactly equal. The frame has no lettering, logos, people, or watermark.
How many requests should you keep in flight?
Two limits apply when you submit work, and both belong to the account. All your keys share them, so a second key raises neither one.
- Rate limit: 120 requests a minute, or one every half second when spread evenly. Treat it as a ceiling: uploads, catalog reads, and balance reads count too.
- Tasks in progress: 100. A task keeps its place until it finishes as
completedorfailed.
So the number to keep in flight is a cap you set in your own code: below 100, and shared by every process on the account. The documentation gives the ceiling, not a recommended number under it, so leave headroom for whatever else uses the account, and start new tasks as places free up, not all at once.
Queue first. Hold each request in a queue of your own.
Count what is in progress. Keep one counter for the whole account, and start the next task only while it is below your cap.
Give the place back. When a task becomes
completedorfailed, lower the counter.Pace the starts. Stay under the rate limit, with room for uploads and reads.
One finished task, like the grid of cups below, gives one place back.
The prompt behind this image
Use a straight overhead view of nine small white handleless ceramic cups in a three-by-three grid. Space them evenly, with equally sized openings. A matte beige ground, even soft lighting and fine contact shadows keep the arrangement clear within an open border. No lettering, logos, people, or watermarks.
Nano Banana 2.1
nano-banana-2.1 · 1K · 1:1 · $0.02
Nano Banana 2.1, 1K, 1:1, one attempt. Nine small off-white cups without handles fill a three-by-three grid, photographed straight from above on a matte beige background. The margins on all four sides are about equal, the cups are about the same size, and every opening is fully visible. The shapes read as small bowls or ramekins more than tall cups, and the shadows are soft and fall beside each cup. Apart from that, we found no departure from the prompt. The frame has no lettering, logos, people, or watermark.
How do you retry after a 429?
Wait, then send the same request again:
- Read
Retry-After. It gives the pause in seconds. Wait at least that long. - Add a small random delay. It keeps your processes from retrying together. The documentation’s example adds up to a second.
- Send the identical request. Same body, same API key, same
X-Idempotency-Key. Then the retry won’t create a second task. - Limit the attempts. The documentation’s example stops after five.
- Stop on
burst_block. It follows repeated401or403responses: fix the key or the account first.
If the first request was accepted, a retry returns the same 202 response, with the same id and no new charge; that saved response may still say queued, so read the current status with GET /creations/{id}. But once an accepted task has become failed, the old key doesn’t start it again. A new attempt is a new request with a new key and a new paid operation, and the failed task’s charge comes back by itself.
A different prompt under an old key returns the old response, because the key doesn’t compare request bodies. A new image takes a new key.
A retry loop in curl, Python, and Node.js
The Python and Node.js loops retry only a 429. After a dropped connection or a lost response, keep the same key and check the task you know before submitting again.
# Requires bash, curl, and uuidgen. Create the key once per operation. IDEMPOTENCY_KEY="$(uuidgen)" || exit 1 curl --include --max-time 30 \ -X POST https://api.rendergrid.io/ api/ public/ v1/ images/ generate \ -H "Authorization: Bearer $RENDERGRID_API_KEY" \ -H "Content-Type: application/json" \ -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \ -d '{ "model": "nano-banana-2.1", "prompt": "A ceramic mug on marble, soft window light", "resolution": "1K", "aspect_ratio": "1:1" }' # On a 429, the Retry-After header in the output is the pause in seconds. # After it, send this command again with the same IDEMPOTENCY_KEY.
import os import random import time import requests API = "https://api.rendergrid.io/ api/ public/ v1" def submit(payload, idempotency_key, max_attempts=5): headers = { "Authorization": f"Bearer {os.environ['RENDERGRID_API_KEY']}", # the same key on every attempt "X-Idempotency-Key": idempotency_key, } for attempt in range(max_attempts): response = requests.post( f"{API}/images/generate", headers=headers, json=payload, timeout=(10, 120) ) if response.status_code != 429: return response try: reason = response.json()["error"]["details"]["reason"] except (ValueError, KeyError, TypeError): reason = None if reason == "burst_block" or attempt + 1 == max_attempts: # a block and the last attempt go back to the caller return response wait = response.headers.get("Retry-After", "") pause = int(wait) if wait.isdigit() else 2 # at least Retry-After, plus a little time.sleep(pause + random.uniform(0, 1)) response = submit( { "model": "nano-banana-2.1", "prompt": "A ceramic mug on marble, soft window light", "resolution": "1K", "aspect_ratio": "1:1", }, idempotency_key="order-8412-cover-v1", ) print(response.status_code, response.json())
// Node.js 22+, .mjs file const API = "https://api.rendergrid.io/ api/ public/ v1"; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function submit(payload, idempotencyKey, maxAttempts = 5) { for (let attempt = 1; ; attempt++) { const response = await fetch(`${API}/images/generate`, { method: "POST", headers: { Authorization: `Bearer ${process.env.RENDERGRID_API_KEY}`, "Content-Type": "application/json", // the same key on every attempt "X-Idempotency-Key": idempotencyKey, }, body: JSON.stringify(payload), signal: AbortSignal.timeout(120_000), }); if (response.status !== 429) return response; const body = await response .clone() .json() .catch(() => ({})); if (body.error?.details?.reason === "burst_block" || attempt === maxAttempts) { // a block and the last attempt go back to the caller return response; } const wait = response.headers.get("Retry-After"); const pause = wait && /^[0-9]+$/.test(wait) ? Number(wait) : 2; // at least Retry-After, plus a little await sleep((pause + Math.random()) * 1000); } } const response = await submit( { model: "nano-banana-2.1", prompt: "A ceramic mug on marble, soft window light", resolution: "1K", aspect_ratio: "1:1", }, "order-8412-cover-v1", ); console.log(response.status, await response.json());
How often can you poll a task?
One task, one key, no more than once every 5 seconds. A poll that comes sooner gets 429 ERR_RATE_LIMIT with details.min_interval_seconds. Wait at least the larger of that number and Retry-After; the header can show a shorter interval. Stop polling at completed or failed.
Polls follow this rule and don’t use up the 120 requests a minute. They still add up: 100 tasks polled every 5 seconds are 20 polls a second.
The documentation promises no waiting time: it depends on the model and the load. Set your own limit on waiting and save the task id; your client’s timeout doesn’t cancel the task.
Why use webhooks instead of frequent polling?
With a webhook, we notify you instead. Add webhook_url to the generation request, and when the task becomes completed or failed we send a signed POST to that address, as a creation.completed or creation.failed event.
Your receiving end needs three things:
- A public HTTPS address of up to 2,048 characters; redirects aren’t followed.
- A signature check. The
X-Webhook-Signature-256header issha256=plus an HMAC-SHA256 of theX-Timestampvalue, a dot, and the raw body, made with the signing secret from the portal. - A quick reply. Respond with
2xxwithin 10 seconds, save the event first, and do downloads afterwards.
If your server doesn’t respond with 2xx, we retry: by default up to five attempts in all, the first at once and the rest 60, 120, 240, and 480 seconds after the one before. X-Webhook-Delivery-ID helps you remove duplicates.
Keep polling as a fallback, because a webhook can be delayed or not arrive. For a full batch setup, see batch generation with webhooks and idempotency; the details are in the webhooks documentation.
What do you get when a task finishes?
Both endings are final. A completed task carries result_urls, the links to the files: download them at once, because files are kept for a day. A failed task carries an error text and a failure object with type, code, and retryable; build your logic on the object.
A failed task isn’t a 429, and its charge comes back to your balance by itself. A new attempt is a new request with a new key. failure.retryable says whether sending the same request again can help; when failure.type is declined, the cause is in the request itself. How refunds work is in the article on failed requests.
The image below shows the first ending: a completed task with its image.
The prompt behind this image
Five identical closed cream jars form a gentle arc on a sand-colored background. From a slightly elevated angle, show equal spacing, smooth round lids and separate soft shadows lit from the right. Keep every jar complete and the inside of the arc empty. No lettering, logos, people, or watermarks.
Nano Banana 2.1
nano-banana-2.1 · 1K · 1:1 · $0.02
Nano Banana 2.1, 1K, 1:1, one attempt. Five closed cream-colored jars with smooth, flat round lids form an arc on a sand-colored textured surface, seen from a raised angle that is steeper than the slightly elevated one the prompt asked for. The arc is symmetric and open toward the viewer, the middle jar is the farthest back, and the inside of the arc is empty. Every jar is complete in the frame. Departures from the prompt: the shadows fall to the left, which fits light from the right, but they are fairly sharp-edged instead of soft, and the shadows of the right-hand jars run together into one dark band, where the prompt asked for separate shadows; two of them fall across the neighboring jars. The frame has no lettering, logos, people, or watermark.
Where does this advice not fit?
- Three images aren’t a measurement. They are ordinary results of accepted requests, one attempt each, and we took no timing or load figures for this article.
- The numbers are defaults. The documentation gives 120 and 100 as default values for any account and says an account’s limit may differ. Contact support if they aren’t enough;
details.limitin a response shows which limit was hit. - Not every rejection is a wait. An empty balance or a used-up monthly limit gets
402 ERR_INSUFFICIENT_BALANCE, and aburst_blockneeds its cause fixed. A short pause fixes neither. - A tidy queue lowers the chance of a 429, not to zero. Other processes share the account’s limits, so keep the retry logic.
A failed request is free
Try a request of your own
Run a prompt in the playground on the Nano Banana 2.1 page, and open its API section when you are ready to build your own queue.
Try Nano Banana 2.1Questions and answers
What does a 429 response mean?
Retry-After, then send the same request again with the same idempotency key.How many requests can I send per minute?
Do polls count toward the request limit?
What should I do after a 429 with burst_block?
Retry-After.Sources
- Rendergrid documentation: LimitsThe rate limit, tasks in progress, polling, and retry rules.
- Rendergrid documentation: Error codesThe error format and the
failureobject. - Rendergrid documentation: Tasks and statusesStatuses, polling, and how long files are kept.
- Rendergrid documentation: WebhooksSetup, signature, and delivery retries.
- Rendergrid documentation: IdempotencyThe
X-Idempotency-Keyheader and what a retry returns. - The images in this articleMade through the Rendergrid API, one request each.





