Firefly Services Rate Limits
The production playbook for operating Firefly Services at scale. Default rate limits are conservative and can be raised per-customer on request; the architecture below is the pattern used in production for any deployment that exceeds a single-user workload — it scales a pipeline from blocked at the default ceiling to enterprise-grade campaign throughput.
When to Use This Skill
Use this skill when:
- A 429 has been seen, or is anticipated for the workload size
- A campaign / batch / pipeline will issue more than ~50 calls in a burst
- The user mentions volume estimates beyond a casual demo
- Designing infrastructure that wraps Firefly Services
- Planning quota with the customer's Adobe account manager
Do NOT use this skill when:
- The workload is genuinely one-shot interactive — direct calls are fine
- A 429 is appearing despite low volume — start with
firefly-services-troubleshoot§3
Default Rate Limits — Know the Numbers
Firefly Services rate limits are per-credential, per-endpoint, with both per-second and per-minute components. Documented defaults at the time of writing — actual provisioned limits are org- and contract-dependent, so verify for your org:
| Endpoint family | Default per credential | Notes |
|---|---|---|
| Generate Image (V3 async) | ~4 RPM | Most common limit; first thing to hit |
| Generate Similar | ~4 RPM | Shares quota family with Generate |
| Generate Expand / Fill | ~4 RPM | Shares quota family with Generate |
| Generate Video | Lower (~1 RPM) | Heavier compute |
| Custom Model training | 1 concurrent job per org | Throughput is training-time bound, not RPM |
| Photoshop API | ~10 RPM | Higher than Firefly |
| Lightroom API | ~10 RPM | Higher than Firefly |
| Token endpoint (IMS) | Not publicly documented — high in practice | Cache tokens aggressively; a token cache that re-auths on every call is a real failure mode (see firefly-services-auth) |
Numbers shift over time and can be raised per-customer via Adobe account management. Verify current limits in the Technical Usage Notes. Adobe officially documents only the 429 status with a Retry-After header for rate limiting; informational headers such as X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset may be present on some responses but are not reliably documented — do not depend on them. (Live check 2026-08-10 on a sandbox credential: an 8-call generate burst completed with zero 429s and no rate-limit headers on any response — enforcement is org-specific and invisible until it triggers, so treat documented defaults as planning numbers and build 429-reactive backoff rather than header-driven throttling.)
The Production Strategy
A real workload that exceeds 50 calls follows this four-layer pattern:
Customer-facing API
↓ (synchronous, low-latency response: "your job is queued")
Job intake → SQS / Pub/Sub / Service Bus
↓ (async worker pool, concurrency-bounded)
Token-bucket limiter (per-credential)
↓ (paces calls within the rate envelope)
Firefly Services
↓
Result persistence + customer notification (webhook or polling)
Each layer exists for a specific reason — skipping a layer is the most common cause of production pager incidents in these pipelines.
Step 1 — Request a Rate-Limit Increase
This is a non-technical step but it is the first one. Adobe will raise rate limits per-customer on request — you do not need to engineer around the default if the customer has volume to justify it.
| Action | Who |
|---|---|
| Open a support ticket with the customer's Adobe Enterprise account team | Customer's procurement / account owner |
| Provide projected volume: peak RPM, daily call count, business justification | Integration engineering team |
| Adobe responds with a proposed limit; agree and implement | Adobe + Customer |
Adobe raises limits per-customer through the enterprise account team. Provide the projected peak RPM and a clear business justification; the granted limit and the turnaround time are org- and contract-dependent, so budget the lead time into the project plan.
Even with a raised limit, build the architecture below — the limit exists, just at a higher number.
Step 2 — Token-Bucket Limiter (Client-Side)
The lowest layer is a per-credential token-bucket that paces outbound requests to ~80% of the provisioned limit. The 20% headroom absorbs jitter from concurrent workers and Adobe-side response time variance.
Node implementation:
class TokenBucket {
constructor({ ratePerMin, burst = ratePerMin }) {
this.tokensPerMs = ratePerMin / 60_000;
this.maxTokens = burst;
this.tokens = burst;
this.lastRefill = Date.now();
}
async take() {
while (true) {
this.refill();
if (this.tokens >= 1) {
this.tokens -= 1;
return;
}
const msPerToken = 1 / this.tokensPerMs;
await sleep(msPerToken + Math.random() * 50); // jitter
}
}
refill() {
const now = Date.now();
this.tokens = Math.min(
this.maxTokens,
this.tokens + (now - this.lastRefill) * this.tokensPerMs,
);
this.lastRefill = now;
}
}
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
Use ratePerMin = provisionedLimit * 0.8. Set burst to roughly 2 seconds of capacity — enough to absorb microbursts without queue starvation.
For multi-tenant services with per-customer credentials, hold one limiter per client_id. Do not share limiters across credentials.
Step 3 — Exponential Backoff with Jitter (Retry Path)
When a 429 still slips through despite client-side limiting, retry with exponential backoff plus full jitter. Full jitter (random uniform [0, max]) is the right choice — equal jitter still produces thundering herds under heavy load.
async function callWithBackoff(fn, { maxAttempts = 5 } = {}) {
let attempt = 0;
while (true) {
attempt++;
try {
return await fn();
} catch (err) {
const status = err.response?.status ?? err.status;
if (status !== 429 && !(status >= 500 && status < 600)) throw err;
if (attempt >= maxAttempts) throw err;
const retryAfterSec = parseInt(err.response?.headers?.get('retry-after') ?? '0', 10);
const delayMs = retryAfterSec
// Retry-After is a minimum wait — sleep at least that long, plus small additive jitter
? retryAfterSec * 1000 + Math.random() * 1000
// No server hint: exponential backoff with full jitter
: Math.random() * Math.min(60_000, 1000 * 2 ** attempt);
await sleep(delayMs);
}
}
}
Honor Retry-After when present — Adobe knows the exact moment the quota refills. Treat it as a floor: sleep the full Retry-After duration and add jitter on top of it, never below it — a shorter sleep retries before the quota refills and burns an attempt. Full jitter belongs only on the exponential path, where there is no server hint.
Cap attempts at 5. Beyond that, surface the failure to the application — the request is going to dead-letter regardless.
Step 4 — Queue-Fronted Architecture
For any workload above ~100 calls, place a durable queue in front of Firefly. This is non-negotiable. Without it, a transient Firefly slowdown becomes an application outage.
AWS reference architecture
API Gateway / ALB (intake)
↓
Lambda: validate + enqueue (synchronous)
↓
SQS Standard queue (main work) (durable)
↓
Lambda: Firefly worker (concurrency-bounded) (paces calls)
│ - Pulls 1 message at a time
│ - Holds a per-credential TokenBucket
│ - Calls Firefly with backoff
│ - On success: writes result to S3 + DynamoDB
│ - On terminal failure: forwards to DLQ
↓
Result: S3 (images) + DynamoDB (job state) (persistence)
↓
EventBridge → customer webhook OR client polling (notification)
Dead-letter queue (DLQ) attached to main SQS with maxReceiveCount=3
↓
Alarm + manual replay path
Key configuration:
| Component | Setting | Why |
|---|---|---|
| SQS visibility timeout | 6 × max Firefly response time | Worker holds the message until call completes |
| Lambda reserved concurrency | max(1, floor(provisioned RPM / 60)) |
Hard cap on concurrent calls — never 0 (reserved concurrency 0 throttles every invocation). At low provisioned RPM (e.g. the 4 RPM default) this is 1, and the token bucket in Step 2, not Lambda concurrency, does the pacing |
| Lambda batch size | 1 | Per-message error isolation |
| SQS maxReceiveCount | 3 | Move to DLQ after 3 failed attempts |
| DLQ retention | 14 days | Manual replay window |
Equivalent patterns on GCP: Pub/Sub + Cloud Functions, with Cloud Run for the worker if Firefly response times exceed Cloud Functions limits. On Azure: Service Bus + Functions, premium plan for sustained throughput.
Workload sizing example (representative)
| Use case | Estimated daily volume | Peak burst | Provisioned RPM | Architecture |
|---|---|---|---|---|
| Hero-asset generation | low thousands of calls/day | hundreds in a 5-minute window | raised limit (negotiated per customer) | SQS + Lambda worker, concurrency 1 |
| Template-driven compositing | low thousands of calls/day | hundreds in a 5-minute window | shares with above | Same queue, different topic |
| Full campaign batch | tens of thousands of calls across several hours | hundreds-to-thousands per hour | raised limit | Same architecture, multi-worker, runs over hours not seconds |
This architecture has been validated for high-volume template-driven campaign asset production at the scale of tens of thousands of calls in a single campaign run.
Step 5 — Dead-Letter Queue Handling
Every queue-fronted system has a DLQ. The patterns that matter:
| Failure type | Action |
|---|---|
| Adobe-side 5xx (transient) | Replay from DLQ after Adobe status page clears |
| Content-validation 422 (terminal) | Do not replay — the prompt or input was rejected; surface to customer |
| 403 (entitlement) | Do not replay — credentials are wrong; alert ops |
| Custom-model retired (404) | Do not replay — surface to customer; suggest re-training |
| Storage-reference expired (400312) | Replay only after regenerating fresh references |
The DLQ replay tool must classify the failure before replaying. Bulk-replay of a DLQ without classification will burn quota on requests that will never succeed.
Step 6 — Observability — What to Log
For every Firefly call:
client_id(which credential)- Endpoint + method
- Request UUID / correlation ID
- Wall-clock latency
- HTTP status
Retry-Afterheader on any429(the one rate-limit signal Adobe documents). If informationalX-RateLimit-*headers happen to be present, log them too — but treat them as best-effort, not guaranteed- Token cache hit/miss (was a new token fetched)
- Retry count if applicable
Dashboards to build:
- p50 / p95 / p99 latency per endpoint
- 429 rate per credential per hour
- DLQ depth and arrival rate
- Token cache hit ratio (should be >99%)
A 429 rate above 1% is the canary for an under-provisioned credential. A token cache hit ratio below 99% means refresh logic is broken.
Validate
Rate-limit architecture is production-ready when:
- The provisioned RPM is documented and a token-bucket limiter paces calls to 80% of it
- Every Firefly call goes through exponential-backoff retry with
Retry-Afterhonoring - Any workload >100 calls is queue-fronted (not direct)
- A DLQ exists with a documented replay procedure
- The 429 rate metric is monitored and alerts at 1%
- Observability captures the
Retry-Afterheader on every429(the documented signal); anyX-RateLimit-*headers are logged opportunistically but not relied upon
Troubleshooting & Edge Cases
- 429s spiking after a code deploy: Likely concurrent workers exceeding the limit. Check Lambda reserved concurrency or your worker pool size; reduce to match
max(1, floor(provisionedRPM / 60)). - 429s coming from a single credential while others are fine: Per-credential limit. Either request a raise for that credential or split the workload across more credentials.
Retry-Afterheader missing on 429: Some legacy endpoints don't return it. Fall back to exponential backoff with cap.- Custom-model jobs queue but never run: Custom-model training is concurrency 1 per org. Other jobs queue behind it; this is by design.
- Photoshop / Lightroom calls hitting 429s while Firefly calls are fine: They have separate quotas. Treat as separate limiters.
- Burst at start of day, then steady: Cold cache. Pre-warm the token cache during deploy / startup.
Chaining with Other Skills
firefly-services-auth— Token caching is half of the rate-limit storyfirefly-services-troubleshoot— 429 deep-divefirefly-generate-image-v3-async— The async pattern is the right shape for high-volume