Illustrazione: code separate per immagini e video lasciano avanzare ogni carico al proprio ritmo.
Un generatore batch di solito cede nel momento meno interessante: non mentre genera una singola immagine, ma quando un cliente importa 20.000 righe e l'applicazione le trasforma tutte insieme in richieste attive.
L'API può essere in salute e ogni payload valido. Il sistema collassa comunque: le connessioni al database si esauriscono, i callback arrivano più in fretta di quanto riesci a fare il commit, un grande tenant blocca tutti gli altri, un retry duplica un video costoso e un picco di traffico diventa un conto.
Tieni il lavoro in attesa in una coda durevole, limita i task in corso e riprova solo quando sai se esiste già un task remoto. Un Promise.all più grande non fa nulla di tutto questo.
Un fatto determina tutto quello che segue: SpicyAPI non ha un endpoint bulk o batch. Ogni riga è una chiamata createTask con il proprio task, il proprio hold e il proprio regolamento, e un task accettato non può essere annullato. La coda, il tetto di concorrenza e il pulsante di stop vivono quindi tutti nella tua applicazione.
Prova prima nel Playground
Apri il Playground di z-image. Scrivi un Prompt rappresentativo, scegli l'Aspect ratio, imposta Prompt expansion come intendi eseguire il batch, premi Run model e controlla l'Output. Scala solo quando quel singolo risultato è quello che vuoi; uno scheduler moltiplica la richiesta, errori compresi.
La risposta breve
- Tieni l'arretrato nella tua coda durevole, non in una richiesta HTTP né in una promise in memoria.
- Limita tu i task in corso. Il rate limit della piattaforma (1.000 richieste ogni 10 secondi, separatamente per letture e scritture) non limita quanti task accettati girano insieme; lo fanno il tuo saldo e i tuoi tetti di spesa. Scegli un numero, per classe di media, e mantienilo finché il task non è terminale.
- Ammetti in base all'età della coda, non alla sua lunghezza, e decidi il costo dell'intero batch prima del primo dispatch.
- Un'idempotency key salvata per ogni output logico, sempre inviata con la stessa API key e mai considerata affidabile oltre le 24 ore.
- Riprova in base al codice di errore: errori di capacità e di trasporto sì; input non valido, saldo esaurito e tetti di spesa no.
- Round-robin fra i tenant, e un kill switch del dispatch che lascia attiva la gestione dei completamenti.
Tre limiti e chi li imposta
Tratta invio, dispatch e completamento come fasi separate:
batch del cliente
│
▼
ammissione + controllo costi ──► coda durevole ──► worker limitati ──► createTask
│
completamento via polling / webhook
│
copia dell'output ──► archivio risultati + evento al cliente
| Limite | Cosa limita | Chi lo imposta su SpicyAPI |
|---|---|---|
| Ammissione | Quanto arretrato tieni e prometti di completare | Tu |
| Frequenza di invio | Chiamate createTask al secondo | Piattaforma: 1.000 scritture ogni 10 secondi per account, un token bucket di circa 100 richieste al secondo, con X-RateLimit-Remaining in ogni risposta e Retry-After su un 429 |
| Task in corso | Task accettati e non finiti | Soprattutto tu. La piattaforma li limita solo tramite i fondi che ogni task trattiene, i tetti di spesa di chiave, team e piattaforma (40202) e la capacità del modello (50301) |
Il rate limit regola l'invio, non la concorrenza: i task accettati non attingono più al bucket delle richieste mentre generano.
A 5 completamenti al secondo crei circa 5 task al secondo, una piccola frazione del bucket da 100 al secondo. Per la maggior parte dei batch il limite di invio è un fusibile di sicurezza, non il tuo acceleratore; prima ti fermano il denaro trattenuto dai task in corso e i tuoi tetti. Il polling usa il bucket di lettura separato, quindi un ciclo di polling fuori controllo non può bloccare createTask, ma può iniziare a restituire 429 su recordInfo e lasciarti alla cieca. Leggi l'header invece di scrivere nel codice i valori predefiniti.
Altre API possono imporre limiti distinti di concorrenza e di coda in attesa; la guida alla concorrenza di Leonardo li distingue. SpicyAPI non documenta un tetto numerico ai task in corso, quindi configura il tuo invece di affidarti al suo rate limit sulle richieste.
Dimensiona il tetto in corso con tempo e denaro
Per una singola classe di carico stabile, la legge di Little mette in relazione il numero medio di task nel sistema con la frequenza di completamento e il tempo medio che ogni task vi trascorre:
task in corso medi ≈ completamenti al secondo × secondi medi dall'accettazione allo stato terminale
Se i task di immagine durano in media 8 secondi e te ne servono 5 al secondo:
5 job/s × 8 s = 40 task di immagine concorrenti
Considera 40 un'occupazione media, non una garanzia. Un tetto iniziale di 50 aggiunge il 25 % di margine; alzalo solo finché età della coda, errori e spesa restano sani. Usa p95 e p99 per le code lunghe e per gli allarmi sull'età della coda. Misura la durata di un task da createdAt e completedAt in recordInfo, per modello e impostazioni.
Il video merita un pool a sé. Se le clip di Seedance 2.0 durano in media 70 secondi nel tuo carico e ne vuoi 0,2 al secondo:
0,2 job/s × 70 s = 14 task video concorrenti
Le medie di 8 e 70 secondi sono ipotesi di dimensionamento, non tempi di generazione promessi.
Con un solo pool condiviso da 50, un'ondata di video lunghi può occupare ogni slot mentre le immagini rapide aspettano. Con 50 slot per le immagini e 14 per i video, entrambe le classi avanzano. Anche Z-Image e Seedream 5.0 Pro possono avere pool separati se durate o priorità differiscono.
Ora confronta il tetto con il denaro. Ogni task accettato trattiene il proprio estimatedCost finché non viene regolato, e quel hold conta per ogni tetto di chiave e di team. Con i prezzi pubblici del catalogo:
| In corso | Trattenuto insieme |
|---|---|
| 50 immagini Seedream 5.0 Pro in 1K ($0.036 ciascuna) | $1.80 |
| 14 clip da testo a video Seedance 2.0, 5 s in 720p ($0.152 al secondo, $0.76 ciascuna) | $10.64 |
Un saldo che non copre quegli hold fa fallire i nuovi task con 40201, per quanto sia bravo il tuo scheduler. Conta di più la spesa totale: 20.000 immagini Seedream 5.0 Pro in 1K fanno $720 e 20.000 immagini Z-Image a $0.01 con Prompt expansion disattivato fanno $200. I prezzi cambiano, quindi leggili dalla pagina del modello aggiornata o da un preventivo. Il tetto che ferma un batch sbagliato è quello che imposti prima di partire: fissa un tetto di spesa giornaliero sulla chiave di produzione prima del dispatch, dimensionato sul massimo che puoi permetterti di perdere in una brutta giornata. Un tetto vuoto significa nessun limite; anche i limiti del team possono fermare nuovi task con 40202.
La stima del tempo di smaltimento è l'altra metà:
tempo di smaltimento ≈ job in coda ÷ completamenti sostenibili al secondo
18.000 immagini in coda a 5 al secondo sono un'ora di lavoro. Se prometti un avvio entro 15 minuti, l'ammissione deve rallentare molto prima di 18.000. Un arretrato che cresce non è uno stato stazionario, quindi fidati dell'età della coda osservata più che delle equazioni.
Il backpressure comincia prima del dispatch
Backpressure significa che il sistema a valle può far rallentare il produttore. Senza, la coda assorbe lavoro all'infinito e il cliente scopre il sovraccarico ore dopo.
Usa due soglie di ammissione sull'età della coda, l'età dell'elemento pronto più vecchio. La lunghezza inganna quando gli elementi hanno costi diversi; l'età dice direttamente se l'attesa promessa viene rispettata.
- Soglia morbida: accetta il batch, restituisci una stima ritardata, rallenta il produttore.
- Soglia dura: rimanda o rifiuta il nuovo lavoro con una risposta di prodotto riprovabile prima che esista qualsiasi task remoto.
Confronta il denaro in unità decimali esatte invece che in virgola mobile binaria. Fai il commit della prenotazione in modo atomico insieme all'ammissione del batch nel tuo adattatore di coda, così invii concorrenti non consumano lo stesso budget residuo.
Non trasformare ogni sovraccarico interno in un 429 dell'API per i tuoi clienti. Se il lavoro è salvato in modo durevole, restituisci un ID di batch accettato in stato ritardato. Rifiuta solo il lavoro che non puoi conservare responsabilmente e, quando lo fai, indica quando riprovare e rendi idempotente l'invio stesso del batch.
Due dettagli di questa API decidono per quanto un elemento può attendere in sicurezza nella tua coda:
- I file di riferimento scadono in un giorno. Un URI
spicy://ottenuto da un upload è valido fino all'expiresAtdella sua risposta di commit (un giorno per impostazione predefinita), e un retry riusa l'input originale, quindi un retry successivo restituisce400. Tieni in coda un URL HTTPS pubblico o un tuo ID sorgente e fai l'upload al momento del dispatch. - I preventivi durano cinque minuti. Se vuoi bloccare il prezzo, chiedi il preventivo al dispatch e invia
quoteIdconexpectedCostacreateTask; un prezzo cambiato fallisce allora con40901invece di addebitare un importo nuovo. I preventivi sono gratuiti ma contano contro il bucket di lettura, quindi chiederne 20.000 all'ammissione è il posto sbagliato.
L'idempotenza ha due livelli
Vanno evitati due duplicati diversi: il cliente che invia lo stesso batch due volte e il tuo dispatcher che non sa se una creazione è riuscita. Dai al batch un'identità stabile, poi genera e salva un UUID per ogni nuovo elemento logico. Il recupero riusa la chiave salvata di quell'elemento.
Mantieni un vincolo di unicità su quella chiave nel database. Scrivi l'elemento come «intento» prima di chiamare l'API, invia la chiave come Idempotency-Key e salva il taskId restituito sulla stessa riga. Dopo un crash, il worker successivo riusa la chiave salvata se l'accettazione è ancora sconosciuta. Scrivere l'intento solo dopo l'invio lascia una finestra in cui un riavvio può comprare lo stesso output due volte.
Ciò che la documentazione sull'idempotenza aggiunge, e che la coda deve rispettare:
- Una chiave è valida 24 ore dalla prima richiesta accettata; i replay non la prolungano. Dopo, la stessa chiave crea un nuovo task fatturato. Prima di rieseguire un vecchio elemento in coda o in dead-letter, cerca il suo
taskIdsalvato invece di fidarti della chiave. - La chiave appartiene allo stesso account e alla stessa API key, e al modello,
inputecallBackUrlesatti. Cambiare qualcosa restituisce409. Anche una chiave sorella del tuo account riceve409e non può leggere il task: solo la chiave che ha creato un task può leggerlo o riprovarlo, quindi usa una sola chiave per creare, fare polling e riprovare in una flotta di worker. - Un replay restituisce il task originale nel suo stato attuale, senza alcun segno che fosse un replay.
- Una rara race con la stessa chiave restituisce
409«Idempotency key conflict, please retry shortly». Riprova una volta dopo qualche centinaio di millisecondi; gli altri due messaggi409non riusciranno mai.
Un dispatcher che mantiene il tetto finché il task non termina
Salva un oggetto per riga in batch.jsonl, per esempio {"model": "alibaba/z-image-turbo/text-to-image", "input": {"prompt": "<one row's prompt>"}}, usando gli input accettati dal modello scelto. Imposta SPICY_API_KEY, BATCH_BUDGET_USD e, facoltativamente, BATCH_WORKERS (2 per impostazione predefinita). Esegui questo esempio Python e approva il tetto del batch. Tiene occupato ogni worker finché il suo task non è terminale, regolato e non sta più preparando l'output.
Conserva la cartella batch-run per il recupero e non riordinare le sue righe sorgente. La stessa cartella mantiene la spesa regolata e le prenotazioni tra un riavvio e l'altro. Esegui un solo coordinatore per cartella.
import json
import os
import time
import uuid
import urllib.error
import urllib.parse
import urllib.request
from concurrent.futures import ThreadPoolExecutor
from decimal import Decimal
from pathlib import Path
from threading import Event, Lock
BASE = "https://api.spicyapi.ai/api/v1"
API_KEY = os.environ["SPICY_API_KEY"]
CAP = Decimal(os.environ["BATCH_BUDGET_USD"])
WORKERS = int(os.environ.get("BATCH_WORKERS", "2"))
ROOT = Path(os.environ.get("BATCH_DIR", "batch-run"))
ROOT.mkdir(exist_ok=True, mode=0o700)
lock, stop = Lock(), Event()
reserved = Decimal("0")
class APIError(Exception):
def __init__(self, status, body):
self.status, self.code = status, body.get("code")
super().__init__(f"{status}/{self.code}: {body.get('msg')}")
def call(path, payload=None, key=None):
headers = {"Authorization": "Bearer " + API_KEY, "Content-Type": "application/json"}
if key:
headers["Idempotency-Key"] = key
for attempt in range(5):
request = urllib.request.Request(BASE + path,
data=None if payload is None else json.dumps(payload).encode(), headers=headers)
try:
with urllib.request.urlopen(request, timeout=30) as response:
body = json.load(response)
if body.get("code") != 200:
raise APIError(response.status, body)
return body["data"]
except urllib.error.HTTPError as error:
try:
body = json.load(error)
except ValueError:
body = {}
if error.code != 429 and error.code < 500:
raise APIError(error.code, body)
delay = float(error.headers.get("Retry-After") or min(15, 2 ** attempt))
except (urllib.error.URLError, TimeoutError, ValueError):
delay = min(15, 2 ** attempt)
time.sleep(delay)
raise RuntimeError("Unresolved API call; preserve this row for recovery")
def save(path, row):
temporary = path.with_suffix(".tmp")
temporary.write_text(json.dumps(row))
temporary.chmod(0o600)
temporary.replace(path)
def worker(pair):
global reserved
index, line = pair
if stop.is_set():
return
try:
request = json.loads(line)
if not isinstance(request.get("model"), str) or not isinstance(request.get("input"), dict):
raise ValueError("model and input are required")
except (ValueError, TypeError, AttributeError) as error:
print(index, "invalid local row:", error)
return
path = ROOT / f"{index}.json"
row = json.loads(path.read_text()) if path.exists() else {
"request": request, "key": str(uuid.uuid4())}
if row["request"] != request:
stop.set()
raise ValueError("Batch changed; preserve the old folder and use a new one")
if row.get("settled") or row.get("rejected"):
return
accounted = "sentAt" in row
try:
if not row.get("taskId"):
if time.time() - row.get("sentAt", time.time()) >= 23 * 3600:
raise RuntimeError("Reconcile old acceptance before submitting again")
if "submission" not in row:
quote = call("/jobs/quote", request)
row["hold"] = quote["maxCharge"]
row["submission"] = {**request, "quoteId": quote["quoteId"],
"expectedCost": quote["estimatedCost"]}
with lock:
if stop.is_set():
return
hold = Decimal(row["hold"])
if not accounted and reserved + hold > CAP:
stop.set()
print(index, "batch budget reached")
return
if not accounted:
reserved += hold
accounted = True
row.setdefault("sentAt", time.time())
save(path, row)
accepted = call("/jobs/createTask", row["submission"], row["key"])
row["taskId"] = accepted["taskId"]
save(path, row)
task_id = row["taskId"]
for _ in range(3000):
task = call("/jobs/recordInfo?taskId=" + urllib.parse.quote(task_id))
if task["state"] not in {"queued", "running", "succeeded", "failed", "expired", "canceled"}:
raise RuntimeError("Unknown state: keep the reservation")
if task["state"] in {"succeeded", "failed", "expired", "canceled"} and task.get("settled") is True:
assets = task.get("output", {}).get("assets", [])
if task["state"] == "succeeded" and any(a.get("pending") for a in assets):
time.sleep(5)
continue
row["result"], row["settled"] = task, True
save(path, row)
with lock:
reserved += Decimal(task["cost"]) - Decimal(row["hold"])
print(index, task_id, task["state"], task["cost"])
return
time.sleep(5)
raise RuntimeError("Wait interrupted; resume the saved task, not a new generation")
except APIError as error:
if error.status == 400 and error.code in {400, 40003, 40004} and not row.get("taskId"):
row["rejected"] = str(error)
save(path, row)
with lock:
if accounted:
reserved -= Decimal(row.get("hold", "0"))
print(index, "rejected input:", error)
return
stop.set()
print(index, "paused:", error)
except Exception as error:
stop.set()
print(index, "paused:", error)
if CAP <= 0 or not 1 <= WORKERS <= 20:
raise ValueError("Use a positive budget and 1–20 workers")
# Count settled spend from this batch again on restart.
for path in ROOT.glob("*.json"):
row = json.loads(path.read_text())
if row.get("settled"):
reserved += Decimal(row["result"]["cost"])
elif row.get("sentAt") and not row.get("rejected"):
reserved += Decimal(row["hold"])
if input(f"Authorize this batch up to ${CAP}? Type yes: ") != "yes":
raise SystemExit("No tasks submitted")
rows = Path("batch.jsonl").read_text().splitlines()
with ThreadPoolExecutor(max_workers=WORKERS) as pool:
list(pool.map(worker, enumerate(rows)))La documentazione sull'integrazione in produzione copre il deployment. Questo esempio isola le righe non valide, mette in pausa la nuova ammissione per saldo o per errori incerti e limita i retry transitori. I task già accettati continuano a essere raccolti dopo la pausa dell'ammissione. Di proposito non fa alcun retry a pagamento automatico; una riga fallita resta registrata per un nuovo tentativo esplicito dopo il regolamento. Esegui un processo di questo tipo per classe di media e metti il round-robin per tenant davanti al file.
Scheduling equo: proteggi i tenant piccoli
Il primo arrivato, primo servito è equo verso i job, non verso i clienti. Se il tenant A accoda 100.000 elementi al minuto prima che il tenant B ne accodi dieci, B aspetta dietro tutti quelli di A. Tieni una coda per tenant e prendi un elemento per tenant a ogni giro:
giro 1: A1, B1, C1
giro 2: A2, B2, C2
I pesi possono riflettere la capacità acquistata, ma dai a ogni tenant attivo una quota minima. Limita anche i task in corso di ciascun tenant: il tetto globale protegge il tuo account, quello del tenant contiene i vicini rumorosi. All'interno di un tenant, le corsie di priorità (anteprime interattive, batch normali, backfill) valgono la pena solo quando la latenza delle anteprime è un reclamo reale. Se le aggiungi, fai invecchiare la priorità con il tempo in coda, altrimenti un flusso costante di anteprime fa morire di fame un backfill per sempre. Tutto ciò che sta su un account condivide gli stessi due bucket di richieste e lo stesso saldo, quindi l'equità fra tenant è compito tuo, non dell'API.
Classifica i fallimenti prima di riprovare
«Riprova tre volte» non è una policy. L'azione giusta dipende da ciò che sai e dal fatto che un task possa già esistere. Questa tabella usa i codici documentati; i valori di code sono nel corpo della risposta, e i fallimenti dei task arrivano come state: "failed" con un errorCode in una chiamata recordInfo riuscita (code: 200).
| Dove | Segnale | Retry? | Cosa fare |
|---|---|---|---|
| Create | Timeout, connessione interrotta, corpo illeggibile | Sì | Esito sconosciuto. Stessa chiave, stessa API key, backoff con jitter |
| Create | 429 con Retry-After | Sì | Attendi i secondi indicati, aggiungi jitter, riduci la frequenza di dispatch. Non leggerlo come prova che andavi troppo veloce |
| Upload | 429 senza Retry-After (upload in sospeso) | Nessun retry immediato | Completa gli upload in sospeso o attendi la scadenza dei loro ticket prima di chiederne altri |
| Create | 500, 503, 50301 (modello non disponibile) | Sì, con limiti | Backoff con scadenza; 50301 può significare anche nessun prezzo attivo, quindi aggiorna il catalogo |
| Create | 409 race con la stessa chiave | Una volta | Qualche centinaio di ms, stessa chiave. Gli altri 409 indicano payload modificato o altra chiave: fermati |
| Create | 400, 40003, 40004 | No | Campo non valido, combinazione non supportata o tipo/dimensione del file non corrispondente; correggi l'elemento, mostra il messaggio |
| Create | 40201 saldo, 40202 tetto di spesa | No | Metti in pausa il dispatch e avvisa. Riprovare non cambierà un saldo né un tetto |
| Create | 401, 403, 40301 | No | Permesso di chiave o di modello: metti in pausa l'integrazione, avvisa un operatore |
| Task | rate_limited, upstream_unavailable, upstream_failed | Sì | Solo dopo settled: true: POST /api/v1/jobs/retry con una chiave legata al task di origine; crea un nuovo task fatturato. Un retry anticipato restituisce 409 |
| Task | timeout, generation_failed | Forse | Spesso fallisce allo stesso modo. Prova una durata più breve o una risoluzione più bassa, oppure rinuncia |
| Task | invalid_request, unsupported_combination, content_rejected | No | Lo stesso input fallisce di nuovo. Cambia l'input o scegli un altro modello |
| Task | invalid_asset | Nuova revisione | Il file di riferimento non c'è più: ricaricalo, aggiorna l'input e richiedi un preventivo per un nuovo task |
| Locale | Callback ricevuto, la tua transazione è fallita | Sì | Riprova il tuo handler, non la creazione; deduplica per ID di consegna o taskId |
Usa un backoff casuale perché i worker non riprovino all'unisono.
Sposta i fallimenti confermati e gli input rifiutati in una dead-letter queue con la loro classificazione e l'azione di replay sicura. Tieni una creazione ambigua o un task accettato non risolto nella coda di riconciliazione con la sua chiave salvata e il task ID; una scadenza di retry locale non prova che sia fallito. Un task fallito di norma rilascia il proprio hold per intero, ma alcuni esiti content_rejected addebitabili vengono regolati al preventivo, quindi leggi settled e cost invece di presumere un rimborso.
La protezione dei costi appartiene allo scheduler
I limiti di concorrenza proteggono le macchine, non il denaro. Un worker perfettamente stabile può comunque fare il dispatch di un batch insostenibile. Controlla il costo stimato rispetto a ogni involucro prima del dispatch:
consentito = residuo giornaliero del tenant
∩ residuo mensile del tenant
∩ residuo del batch di progetto
∩ residuo orario globale
Riconcilia con il regolamento reale: createTask restituisce l'estimatedCost trattenuto, un task riuscito viene regolato a quell'importo o meno, e settled: true con cost è l'addebito finale. Rilascia la tua prenotazione per gli elementi che l'API ha rifiutato e mantieni la prenotazione per quelli accettati finché settled non cambia. Non tirare a indovinare da un timeout HTTP.
Salvaguardie utili:
- un costo stimato massimo per batch, mostrato a una persona prima che ne parta uno grande;
- un budget di dispatch orario per tenant e un allarme sulla velocità di spesa globale;
- un circuit breaker per modello o tenant;
- tetti delle API key sotto il saldo dell'account e un avviso di saldo basso;
- un kill switch che ferma il nuovo dispatch lasciando attiva l'elaborazione dei completamenti.
Poiché i task accettati non possono essere annullati, fermare la creazione è l'unico freno: il tetto di task in corso, non un pulsante di annullamento, limita i danni in un incidente di costi. Continua a ricevere i webhook, a fare polling dei vecchi task, a regolare le prenotazioni e a copiare gli output.
Il completamento su scala batch
La consegna callBackUrl di ogni task scade dopo 15 secondi; anche un endpoint di account registrato può ricevere la propria consegna, quindi il ricevente dovrebbe verificare la firma sul corpo grezzo, scrivere l'evento in una tabella inbox, restituire 2xx e lasciare il resto a un worker. Deduplica sul request_id di consegna v2 e decidi in base a data.state, non all'ordine di arrivo. Tieni un polling lento come ripiego per gli eventi persi (vedi la documentazione dei webhook).
Copia gli output nel tuo storage man mano che i task finiscono. I link dei risultati durano circa 20 minuti e i media generati vengono conservati al massimo circa 14 giorni (retention); a 5 completamenti al secondo il passo di copia fa parte del tuo throughput, e un uploader lento diventa il collo di bottiglia che si nasconde nei task «completati». Accorciare la retention può anche eliminare i risultati di task che hai già eseguito.
Osserva il flusso, non solo gli errori
Una dashboard utile risponde a quattro domande: stiamo accettando lavoro, sta partendo, sta finendo, il costo viene regolato come previsto? Traccia per modello, classe di media, livello di tenant ed esito:
| Segnale | Perché conta |
|---|---|
| Età dell'elemento pronto più vecchio | Misura diretta dell'attesa del cliente e del fallimento del backpressure |
| Conteggi di elementi in coda, in corso e terminali | Mostra dove si accumula il lavoro |
| Task accettati al secondo vs. create tentate | Separa il throughput reale dai retry |
Durata dei task p50/p95/p99 (completedAt - createdAt) | Guida il dimensionamento della concorrenza e il rilevamento dei task bloccati |
Esiti terminali e mix di errorCode | Degrado specifico di un modello o di un input |
X-RateLimit-Remaining | Avvisa prima di un 429 |
Saldo trattenuto vs. disponibile (GET /api/v1/chat/credit) | Prevede 40201 prima che fermi il batch |
| Costo prenotato vs. regolato, spesa all'ora per tenant | Prenotazioni perse, batch fuori controllo |
| Ritardo dei webhook e recuperi tramite polling | Salute del percorso di completamento |
Porta una sola catena di correlazione (batchId, ID elemento, idempotency key, taskId, request_id) attraverso log ed eventi, e tieni prompt, API key e URL di asset privati fuori dalle etichette delle metriche e dai log di routine. Allerta sui sintomi: «50.000 elementi in coda» può essere normale; «l'elemento interattivo più vecchio ha 12 minuti» richiede un'azione.
Parti con un batch limitato, poi aumenta la capacità
Aumenta la capacità solo finché età dell'elemento più vecchio, fallimenti dei task e spesa restano entro le tue soglie; più worker non possono riparare un percorso di completamento sovraccarico.
Prima del primo batch a pagamento, usa un mock per interrompere un worker subito prima e subito dopo l'invio e per restituire 429, task falliti ed errori di tetto di spesa. Il recupero dovrebbe riusare l'intento salvato, lasciare irrisolti i task sconosciuti e fermare il nuovo dispatch senza fermare il completamento. Poi esegui un piccolo batch approvato prima di aprire l'ammissione a importazioni grandi.
Per la gestione dei completamenti, vedi «Polling vs webhooks for async generation jobs»; per il budget di un batch, vedi Per-unit pricing vs subscriptions. La checklist di produzione copre la configurazione delle chiavi e i test di lancio.
FAQ
SpicyAPI ha un endpoint batch o un limite di concorrenza?
Nessun endpoint batch: ogni riga è un createTask, e non esiste un tetto documentato ai task in corso (i limiti sono nella tabella sopra; la frequenza attuale è in X-RateLimit-Limit). Se il rate limit è troppo basso per il tuo picco, chiedi un limite più alto al supporto della console, descrivendo il picco di richieste al secondo e la dimensione del batch.
Posso annullare un batch avviato per errore?
No. Usa il kill switch del dispatch per fermare i nuovi task mentre la gestione dei completamenti continua.
Un 429 deve consumare un tentativo di retry?
Attendi Retry-After più jitter e mantieni la stessa idempotency key. Un throttling ripetuto richiede una frequenza di dispatch più bassa. Limita i retry sia per tentativi sia per tempo; se l'accettazione è ancora incerta quando quel budget finisce, riconcilia l'intento salvato prima di creare un sostituto.
Come evito addebiti doppi dopo un timeout?
Reinvia con la stessa Idempotency-Key e la stessa API key entro 24 ore, e salva il taskId appena ce l'hai. Dopo 24 ore, cerca invece il taskId salvato.
Una coda basta a prevenire il sovraccarico?
No. Una coda conserva il sovraccarico; il backpressure lo controlla. Servono comunque soglie di ammissione sull'età della coda, un tetto ai task in corso e una risposta di prodotto per quando l'attesa promessa non può essere rispettata.
Qual è una buona concorrenza iniziale?
Completamenti al secondo per il tempo medio di task, per classe di media, più circa il 25 % di margine: 40 per 5 immagini al secondo da 8 secondi ciascuna. Alzala solo finché età della coda, errori e spesa restano sani.
I job ad alta priorità possono aggirare i limiti?
No. La priorità sceglie fra il lavoro ammissibile e non aggira mai il tetto globale né i budget. Dai al lavoro interattivo una capacità riservata e fai invecchiare il lavoro a priorità più bassa perché non muoia di fame.
Lascia che un Agent gestisca la chiamata
Usa un Agent quando vuoi che prepari gli input e restituisca il risultato mentre tu approvi il prezzo e le scelte creative.
- CLI: con l'oggetto
inputdi una riga salvato comeinput.json(per Z-Image:prompt,aspect_ratio,enable_prompt_expansion,output_format,seed), usanpx @spicyapi/cli tasks create --model alibaba/z-image-turbo/text-to-image --input-file input.json --wait. La CLI conferma il prezzo; conserva il task ID restituito per il recupero. - MCP: chiedi al tuo assistente di usare
spicyapi_task_createperalibaba/z-image-turbo/text-to-image, poispicyapi_task_wait. Il server MCP richiede la conferma del prezzo prima della creazione. - Skill: installala con
npx skills add spicyapi-ai/spicy-skill, poi chiedi a$spicyapidi eseguire lo stesso workflow e di fermarsi per la tua approvazione. La Skill usa una connessione CLI o MCP configurata.








