Illustration: Getrennte Bild- und Video-Queues lassen jede Last in ihrem eigenen Tempo vorankommen.
Ein Batch-Generator scheitert meist im unspektakulärsten Moment: nicht beim Erzeugen eines einzelnen Bildes, sondern wenn ein Kunde 20.000 Zeilen importiert und die Anwendung alle auf einmal in laufende Requests verwandelt.
Die API kann gesund und jede Payload gültig sein. Das System bricht trotzdem zusammen: Datenbankverbindungen sind ausgeschöpft, Callbacks treffen schneller ein, als Sie sie committen können, ein großer Tenant blockiert alle anderen, ein Retry verdoppelt ein teures Video, und eine Lastspitze wird zur Rechnung.
Halten Sie wartende Arbeit in einer dauerhaften Queue, begrenzen Sie die Tasks in flight und wiederholen Sie nur, wenn Sie wissen, ob schon ein Remote-Task existiert. Ein größeres Promise.all leistet nichts davon.
Eine Tatsache prägt alles Folgende: SpicyAPI hat keinen Bulk- oder Batch-Endpunkt. Jede Zeile ist ein eigener createTask-Aufruf mit eigenem Task, eigenem Hold und eigener Abrechnung, und ein akzeptierter Task kann nicht abgebrochen werden. Die Queue, das Parallelitätslimit und der Stopp-Knopf liegen daher alle in Ihrer Anwendung.
Zuerst im Playground ausprobieren
Öffnen Sie den z-image Playground. Schreiben Sie einen repräsentativen Prompt, wählen Sie das Aspect ratio, stellen Sie Prompt expansion so ein, wie Sie den Batch fahren wollen, klicken Sie auf Run model und prüfen Sie den Output. Skalieren Sie erst hoch, wenn dieses einzelne Ergebnis stimmt; ein Scheduler vervielfacht die Anfrage samt ihren Fehlern.
Die kurze Antwort
- Halten Sie den Rückstand in Ihrer eigenen dauerhaften Queue, nicht in einem HTTP-Request oder einem In-Memory-Promise.
- Begrenzen Sie die Tasks in flight selbst. Das Rate-Limit der Plattform (1.000 Requests pro 10 Sekunden, getrennt für Lese- und Schreibzugriffe) begrenzt nicht, wie viele akzeptierte Tasks gleichzeitig laufen; das tun Ihr Guthaben und Ihre Ausgabenlimits. Wählen Sie pro Medienklasse eine Zahl und halten Sie sie, bis der Task terminal ist.
- Lassen Sie nach Queue-Alter zu, nicht nach Queue-Länge, und legen Sie die Kosten des ganzen Batches vor dem ersten Dispatch fest.
- Ein gespeicherter Idempotency-Key pro logischer Ausgabe, immer mit demselben API-Key gesendet und nie über 24 Stunden hinaus vertraut.
- Retry nach Fehlercode: Kapazitäts- und Transportfehler ja; ungültige Eingaben, leeres Guthaben und Ausgabenlimits nein.
- Round-Robin über Tenants, plus ein Kill-Switch für den Dispatch, der die Verarbeitung der Abschlüsse weiterlaufen lässt.
Drei Limits und wer sie setzt
Behandeln Sie Einreichung, Dispatch und Abschluss als getrennte Stufen:
Client-Batch
│
▼
Zulassung + Kostenprüfung ──► dauerhafte Queue ──► begrenzte Worker ──► createTask
│
Polling / Webhook-Abschluss
│
Output kopieren ──► Ergebnisspeicher + Kundenereignis
| Limit | Was es begrenzt | Wer es auf SpicyAPI setzt |
|---|---|---|
| Zulassung | Wie viel Rückstand Sie halten und zu erledigen versprechen | Sie |
| Einreichungsrate | createTask-Aufrufe pro Sekunde | Plattform: 1.000 Schreibzugriffe pro 10 Sekunden je Konto, ein Token Bucket von etwa 100 Requests pro Sekunde, mit X-RateLimit-Remaining in jeder Antwort und Retry-After bei einem 429 |
| Tasks in flight | Akzeptierte, unfertige Tasks | Größtenteils Sie. Die Plattform begrenzt sie nur über die Mittel, die jeder Task hält, die Ausgabenlimits von Key, Team und Plattform (40202) und die Modellkapazität (50301) |
Das Rate-Limit steuert die Einreichung, nicht die Parallelität: Akzeptierte Tasks beziehen während der Generierung nichts mehr aus dem Request-Bucket.
Bei 5 Abschlüssen pro Sekunde erzeugen Sie etwa 5 Tasks pro Sekunde, einen kleinen Bruchteil des Buckets von 100 pro Sekunde. Für die meisten Batches ist das Einreichungslimit eine Sicherung, keine Drossel; als Erstes setzen das Geld, das Tasks in flight halten, und Ihre eigenen Limits Grenzen. Das Polling nutzt den separaten Lese-Bucket, sodass eine außer Kontrolle geratene Poll-Schleife createTask nicht blockieren kann, wohl aber 429 bei recordInfo auslösen und Sie blind machen kann. Lesen Sie den Header, statt die Standardwerte fest zu codieren.
Andere APIs können getrennte Limits für Parallelität und wartende Queue vorgeben; Leonardos Concurrency-Leitfaden unterscheidet sie. SpicyAPI hat keine dokumentierte Obergrenze für Tasks in flight, richten Sie daher Ihre eigene ein, statt sich auf das Request-Rate-Limit zu verlassen.
Das Limit für Tasks in flight aus Zeit und Geld ableiten
Für eine stabile Workload-Klasse setzt das Little'sche Gesetz die durchschnittliche Zahl der Tasks im System in Beziehung zur Abschlussrate und zur mittleren Zeit, die jeder Task dort verbringt:
durchschnittliche Tasks in flight ≈ Abschlüsse pro Sekunde × mittlere Sekunden von der Annahme bis zum terminalen Zustand
Wenn Bild-Tasks im Schnitt 8 Sekunden dauern und Sie 5 pro Sekunde brauchen:
5 Jobs/s × 8 s = 40 gleichzeitige Bild-Tasks
Verstehen Sie 40 als mittlere Auslastung, nicht als Garantie. Ein Startlimit von 50 gibt 25 % Spielraum; erhöhen Sie es nur, solange Queue-Alter, Fehler und Ausgaben im grünen Bereich bleiben. Nutzen Sie p95 und p99 für die Ausreißer und für Alarme auf das Queue-Alter. Messen Sie die Task-Dauer aus createdAt und completedAt in recordInfo, je Modell und Einstellung.
Video gehört in einen eigenen Pool. Wenn Seedance 2.0-Clips in Ihrer Workload im Schnitt 70 Sekunden brauchen und Sie 0,2 pro Sekunde wollen:
0,2 Jobs/s × 70 s = 14 gleichzeitige Video-Tasks
Die Durchschnitte von 8 und 70 Sekunden sind Annahmen für die Dimensionierung, keine zugesagten Generierungszeiten.
Teilen sich beide einen Pool von 50, kann eine Welle langer Videos jeden Slot belegen, während schnelle Bilder warten. Mit 50 Bild-Slots und 14 Video-Slots kommen beide Klassen voran. Z-Image und Seedream 5.0 Pro können ebenfalls eigene Pools bekommen, wenn sich Dauer oder Priorität unterscheiden.
Prüfen Sie das Limit nun gegen das Geld. Jeder akzeptierte Task hält seine estimatedCost, bis er abgerechnet ist, und der Hold zählt für jedes Key- und Team-Limit. Mit öffentlichen Katalogpreisen:
| In flight | Gleichzeitig gehalten |
|---|---|
| 50 Seedream 5.0 Pro-Bilder in 1K (je $0.036) | $1.80 |
| 14 Seedance 2.0 Text-zu-Video-Clips, 5 s in 720p ($0.152 pro Sekunde, je $0.76) | $10.64 |
Ein Guthaben, das diese Holds nicht deckt, lässt neue Tasks mit 40201 fehlschlagen, egal wie gut Ihr Scheduler ist. Wichtiger sind die Gesamtausgaben: 20.000 Seedream 5.0 Pro-Bilder in 1K kosten $720 und 20.000 Z-Image-Bilder zu $0.01 ohne Prompt expansion $200. Preise ändern sich, lesen Sie sie daher von der aktuellen Modellseite oder aus einem Quote. Das Limit, das einen schlechten Batch stoppt, ist das, das Sie vor dem Start setzen: Legen Sie vor dem Dispatch ein tägliches Ausgabenlimit für den Produktions-Key fest, bemessen nach dem Höchstbetrag, den Sie an einem schlechten Tag verlieren können. Ein leeres Limit bedeutet kein Limit; auch Team-Limits können neue Tasks mit 40202 stoppen.
Die Abschätzung der Abarbeitungszeit ist die andere Hälfte:
Abarbeitungszeit ≈ Jobs in der Queue ÷ dauerhaft mögliche Abschlüsse pro Sekunde
18.000 Bilder in der Queue bei 5 pro Sekunde sind eine Stunde Arbeit. Versprechen Sie 15 Minuten bis zum Start, muss die Zulassung deutlich vor 18.000 bremsen. Ein wachsender Rückstand ist kein Beharrungszustand, verlassen Sie sich daher mehr auf das beobachtete Queue-Alter als auf die Formeln.
Backpressure beginnt vor dem Dispatch
Backpressure heißt, dass das nachgelagerte System den Produzenten ausbremsen kann. Ohne sie nimmt die Queue endlos Arbeit auf, und der Kunde erfährt erst Stunden später von der Überlast.
Verwenden Sie zwei Zulassungsschwellen auf das Queue-Alter, das Alter des ältesten bereiten Eintrags. Die Länge täuscht, wenn Einträge unterschiedlich teuer sind; das Alter zeigt direkt, ob die zugesagte Wartezeit eingehalten wird.
- Weiche Schwelle: den Batch annehmen, eine verzögerte Schätzung zurückgeben, den Produzenten bremsen.
- Harte Schwelle: neue Arbeit mit einer wiederholbaren Produktantwort zurückstellen oder ablehnen, bevor ein Remote-Task existiert.
Vergleichen Sie Geldbeträge in exakten Dezimaleinheiten statt in binärem Fließkomma. Committen Sie die Reservierung atomar mit der Batch-Zulassung in Ihrem Queue-Adapter, damit gleichzeitige Einreichungen nicht dasselbe Restbudget verbrauchen.
Machen Sie nicht jede interne Überlast zu einem API-429 für Ihre eigenen Kunden. Ist die Arbeit dauerhaft gespeichert, geben Sie eine akzeptierte Batch-ID in einem verzögerten Zustand zurück. Lehnen Sie nur Arbeit ab, die Sie nicht verantwortungsvoll halten können, und nennen Sie dabei einen Wiederholungszeitpunkt und machen Sie die Batch-Einreichung selbst idempotent.
Zwei Details dieser API entscheiden, wie lange ein Eintrag sicher in Ihrer Queue warten darf:
- Referenzdateien laufen nach einem Tag ab. Eine
spicy://-URI aus einem Upload gilt bis zumexpiresAtin ihrer Commit-Antwort (standardmäßig einen Tag), und ein Retry verwendet die ursprüngliche Eingabe, sodass ein Retry danach400liefert. Halten Sie eine öffentliche HTTPS-URL oder Ihre eigene Quell-ID in der Queue und laden Sie erst beim Dispatch hoch. - Quotes gelten fünf Minuten. Wenn Sie den Preis sperren wollen, holen Sie den Quote beim Dispatch und senden Sie
quoteIdmitexpectedCostancreateTask; ein geänderter Preis scheitert dann mit40901, statt einen neuen Betrag zu berechnen. Quotes sind kostenlos, zählen aber gegen den Lese-Bucket, daher ist die Zulassung der falsche Ort, 20.000 Zeilen zu quoten.
Idempotenz hat zwei Ebenen
Zwei verschiedene Duplikate gilt es zu verhindern: der Kunde, der denselben Batch zweimal einreicht, und Ihr Dispatcher, der nicht weiß, ob ein Create gelungen ist. Geben Sie dem Batch eine stabile Identität und erzeugen und speichern Sie dann für jeden neuen logischen Eintrag eine UUID. Die Wiederherstellung verwendet den gespeicherten Key dieses Eintrags erneut.
Legen Sie in Ihrer Datenbank eine Unique-Constraint auf diesen Key an. Schreiben Sie den Eintrag als „Intent“, bevor Sie die API aufrufen, senden Sie den Key als Idempotency-Key und speichern Sie die zurückgegebene taskId in derselben Zeile. Nach einem Absturz verwendet der nächste Worker den gespeicherten Key erneut, wenn die Annahme noch unbekannt ist. Wer den Intent erst nach dem Senden schreibt, lässt ein Fenster offen, in dem ein Neustart dieselbe Ausgabe zweimal kauft.
Was die Idempotenz-Dokumentation ergänzt und die Queue beachten muss:
- Ein Key gilt 24 Stunden ab der ersten akzeptierten Anfrage; Replays verlängern ihn nicht. Danach erzeugt derselbe Key einen neuen, abgerechneten Task. Bevor Sie einen alten Queue- oder Dead-Letter-Eintrag erneut abspielen, schlagen Sie seine gespeicherte
taskIdnach, statt dem Key zu vertrauen. - Der Key gehört zum selben Konto und demselben API-Key sowie zu genau diesem Modell,
inputundcallBackUrl. Jede Änderung liefert409. Ein Schwester-Key Ihres Kontos erhält ebenfalls409und kann den Task nicht lesen: Nur der Key, der einen Task erzeugt hat, kann ihn lesen oder wiederholen. Verwenden Sie daher in einer Worker-Flotte einen Key für Create, Poll und Retry. - Ein Replay liefert den ursprünglichen Task in seinem aktuellen Zustand, ohne Markierung, dass es ein Replay war.
- Ein seltenes Rennen mit demselben Key liefert
409„Idempotency key conflict, please retry shortly“. Wiederholen Sie einmal nach ein paar hundert Millisekunden; die beiden anderen409-Meldungen gelingen nie.
Ein Dispatcher, der das Limit hält, bis der Task endet
Speichern Sie ein Objekt pro Zeile in batch.jsonl, zum Beispiel {"model": "alibaba/z-image-turbo/text-to-image", "input": {"prompt": "<one row's prompt>"}}, mit den akzeptierten Eingaben des gewählten Modells. Setzen Sie SPICY_API_KEY, BATCH_BUDGET_USD und optional BATCH_WORKERS (Standard 2). Führen Sie dieses Python-Beispiel aus und bestätigen Sie das Batch-Limit. Es hält jeden Worker besetzt, bis sein Task terminal und abgerechnet ist und keinen Output mehr vorbereitet.
Bewahren Sie den Ordner batch-run für die Wiederherstellung auf und ändern Sie die Reihenfolge seiner Quellzeilen nicht. Derselbe Ordner behält abgerechnete Ausgaben und Reservierungen über Neustarts hinweg. Starten Sie nur einen Koordinator pro Ordner.
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)))Die Dokumentation zur Produktionsintegration behandelt das Deployment. Dieses Beispiel isoliert ungültige Zeilen, pausiert die neue Zulassung bei Guthabenproblemen oder unsicheren Fehlern und begrenzt transiente Retries. Bereits akzeptierte Tasks werden nach dem Pausieren der Zulassung weiter eingesammelt. Es führt bewusst keinen automatischen kostenpflichtigen Retry aus; eine fehlgeschlagene Zeile bleibt für einen ausdrücklichen neuen Versuch nach der Abrechnung erfasst. Starten Sie einen solchen Prozess pro Medienklasse und setzen Sie das Tenant-Round-Robin vor die Datei.
Faires Scheduling: kleine Tenants schützen
First-in-first-out ist fair gegenüber Jobs, nicht gegenüber Kunden. Stellt Tenant A 100.000 Einträge pro Minute ein, bevor Tenant B zehn einstellt, wartet B hinter allen von A. Führen Sie eine Queue pro Tenant und nehmen Sie pro Runde einen Eintrag je Tenant:
Runde 1: A1, B1, C1
Runde 2: A2, B2, C2
Gewichte können gekaufte Kapazität abbilden, aber geben Sie jedem aktiven Tenant einen Mindestanteil. Begrenzen Sie außerdem die Tasks in flight je Tenant: Das globale Limit schützt Ihr Konto, das Tenant-Limit dämmt laute Nachbarn ein. Innerhalb eines Tenants lohnen sich Prioritätsspuren (interaktive Vorschauen, normale Batches, Backfills) erst, wenn die Latenz der Vorschauen ein echtes Problem ist. Wenn Sie sie einführen, lassen Sie die Priorität mit der Wartezeit altern, sonst verhungert ein Backfill hinter einem steten Strom von Vorschauen. Alles auf einem Konto teilt dieselben zwei Request-Buckets und dasselbe Guthaben, die Fairness zwischen Tenants ist also Ihre Aufgabe, nicht die der API.
Fehler klassifizieren, bevor Sie wiederholen
„Dreimal wiederholen“ ist keine Richtlinie. Die richtige Aktion hängt davon ab, was Sie wissen und ob ein Task schon existieren kann. Diese Tabelle nutzt die dokumentierten Codes; code-Werte stehen im Antwort-Body, und Task-Fehler kommen als state: "failed" mit einem errorCode bei einem erfolgreichen recordInfo-Aufruf (code: 200).
| Wo | Signal | Retry? | Was zu tun ist |
|---|---|---|---|
| Create | Timeout, abgebrochene Verbindung, unlesbarer Body | Ja | Ergebnis unbekannt. Gleicher Key, gleicher API-Key, Backoff mit Jitter |
| Create | 429 mit Retry-After | Ja | Die genannten Sekunden warten, Jitter addieren, Dispatch-Rate senken. Nicht als Beweis lesen, dass Sie zu schnell waren |
| Upload | 429 ohne Retry-After (ausstehende Uploads) | Kein sofortiger Retry | Ausstehende Uploads abschließen oder warten, bis ihre Tickets ablaufen, bevor Sie weitere anfordern |
| Create | 500, 503, 50301 (Modell nicht verfügbar) | Ja, begrenzt | Backoff mit Deadline; 50301 kann auch „kein aktiver Preis“ bedeuten, aktualisieren Sie also den Katalog |
| Create | 409 Rennen mit demselben Key | Einmal | Ein paar hundert ms, gleicher Key. Andere 409 bedeuten geänderte Payload oder anderen Key: stoppen |
| Create | 400, 40003, 40004 | Nein | Ungültiges Feld, nicht unterstützte Kombination oder Dateityp-/Größenkonflikt; den Eintrag korrigieren, die Meldung anzeigen |
| Create | 40201 Guthaben, 40202 Ausgabenlimit | Nein | Dispatch pausieren und alarmieren. Ein Retry ändert weder Guthaben noch Limit |
| Create | 401, 403, 40301 | Nein | Key- oder Modellberechtigung: Integration pausieren, einen Operator alarmieren |
| Task | rate_limited, upstream_unavailable, upstream_failed | Ja | Erst nach settled: true: POST /api/v1/jobs/retry mit einem an den Quell-Task gebundenen Key; erzeugt einen neuen abgerechneten Task. Ein früherer Retry liefert 409 |
| Task | timeout, generation_failed | Vielleicht | Scheitert oft genauso. Kürzere Dauer oder niedrigere Auflösung versuchen, oder aufgeben |
| Task | invalid_request, unsupported_combination, content_rejected | Nein | Dieselbe Eingabe scheitert erneut. Eingabe ändern oder ein anderes Modell wählen |
| Task | invalid_asset | Neue Revision | Referenzdatei ist weg: erneut hochladen, Eingabe aktualisieren und einen neuen Task bepreisen |
| Lokal | Callback empfangen, Ihre Transaktion schlug fehl | Ja | Ihren Handler wiederholen, nicht das Create; nach Delivery-ID oder taskId deduplizieren |
Verwenden Sie zufälliges Backoff, damit Worker nicht im Gleichschritt wiederholen.
Verschieben Sie bestätigte Fehler und abgelehnte Eingaben mit ihrer Klassifizierung und der sicheren Replay-Aktion in eine Dead-Letter-Queue. Halten Sie ein mehrdeutiges Create oder einen ungeklärten akzeptierten Task in der Abgleichs-Queue mit gespeichertem Key und Task-ID; eine lokale Retry-Deadline beweist nicht, dass er fehlgeschlagen ist. Ein fehlgeschlagener Task gibt seinen Hold normalerweise vollständig frei, aber manche kostenpflichtigen content_rejected-Ergebnisse werden zum Quote abgerechnet. Lesen Sie daher settled und cost, statt eine Erstattung anzunehmen.
Kostenschutz gehört in den Scheduler
Parallelitätslimits schützen Maschinen, nicht Geld. Ein vollkommen stabiler Worker kann dennoch einen unbezahlbaren Batch dispatchen. Prüfen Sie die geschätzten Kosten vor dem Dispatch gegen jede Obergrenze:
erlaubt = Tenant-Tagesrest
∩ Tenant-Monatsrest
∩ Projekt-Batch-Rest
∩ globaler Stundenrest
Gleichen Sie mit der tatsächlichen Abrechnung ab: createTask liefert die gehaltene estimatedCost, ein erfolgreicher Task wird zu oder unter diesem Betrag abgerechnet, und settled: true mit cost ist die endgültige Belastung. Geben Sie Ihre eigene Reservierung für Einträge frei, die die API abgelehnt hat, und behalten Sie die Reservierung für akzeptierte, bis settled umspringt. Raten Sie nicht anhand eines HTTP-Timeouts.
Nützliche Schutzmaßnahmen:
- ein Höchstbetrag an geschätzten Kosten pro Batch, einem Menschen angezeigt, bevor ein großer startet;
- ein stündliches Dispatch-Budget pro Tenant und ein Alarm auf die globale Ausgabengeschwindigkeit;
- ein Circuit Breaker pro Modell oder Tenant;
- API-Key-Limits unterhalb des Kontoguthabens und ein Alarm bei niedrigem Guthaben;
- ein Kill-Switch, der neuen Dispatch stoppt und die Verarbeitung der Abschlüsse weiterlaufen lässt.
Da akzeptierte Tasks nicht abgebrochen werden können, ist das Stoppen der Erzeugung die einzige Bremse: Das Limit für Tasks in flight, kein Abbrechen-Knopf, begrenzt den Schaden bei einem Kostenvorfall. Empfangen Sie weiter Webhooks, pollen Sie alte Tasks, rechnen Sie Reservierungen ab und kopieren Sie Outputs.
Abschluss im Batch-Maßstab
Die Zustellung per callBackUrl je Task bricht nach 15 Sekunden mit einem Timeout ab; auch ein registrierter Konto-Endpunkt kann eine eigene Zustellung erhalten. Der Empfänger sollte daher die Signatur gegen den Roh-Body prüfen, das Ereignis in eine Inbox-Tabelle schreiben, 2xx zurückgeben und den Rest einem Worker überlassen. Deduplizieren Sie nach der v2-Delivery-request_id und entscheiden Sie nach data.state, nicht nach der Ankunftsreihenfolge. Halten Sie ein langsames Polling als Fallback für verpasste Ereignisse bereit (siehe die Webhook-Dokumentation).
Kopieren Sie Outputs in Ihren eigenen Speicher, sobald Tasks fertig werden. Ergebnis-Links halten etwa 20 Minuten, und generierte Medien werden höchstens etwa 14 Tage aufbewahrt (Retention); bei 5 Abschlüssen pro Sekunde ist der Kopierschritt Teil Ihres Durchsatzes, und ein langsamer Uploader wird zum Engpass, der sich in „abgeschlossenen“ Tasks versteckt. Eine kürzere Retention kann auch Ergebnisse von Tasks entfernen, die Sie bereits ausgeführt haben.
Den Fluss beobachten, nicht nur Fehler
Ein nützliches Dashboard beantwortet vier Fragen: Nehmen wir Arbeit an, startet sie, wird sie fertig, wird die Abrechnung wie erwartet verbucht? Verfolgen Sie nach Modell, Medienklasse, Tenant-Stufe und Ergebnis:
| Signal | Warum es wichtig ist |
|---|---|
| Alter des ältesten bereiten Eintrags | Direktes Maß für die Kundenwartezeit und für versagende Backpressure |
| Zahl der Einträge in Queue, in flight und terminal | Zeigt, wo sich Arbeit staut |
| Akzeptierte Tasks pro Sekunde vs. versuchte Creates | Trennt echten Durchsatz von Retries |
Task-Dauer p50/p95/p99 (completedAt - createdAt) | Bestimmt die Parallelitäts-Dimensionierung und die Erkennung hängender Tasks |
Terminale Ergebnisse und errorCode-Mix | Modell- oder eingabespezifische Verschlechterung |
X-RateLimit-Remaining | Warnt vor einem 429 |
Gehaltenes vs. verfügbares Guthaben (GET /api/v1/chat/credit) | Sagt 40201 voraus, bevor es den Batch anhält |
| Reservierte vs. abgerechnete Kosten, Ausgaben pro Stunde und Tenant | Verwaiste Reservierungen, außer Kontrolle geratene Batches |
| Webhook-Verzögerung und Poll-Wiederherstellungen | Zustand des Abschlusspfads |
Führen Sie eine Korrelationskette (batchId, Eintrags-ID, Idempotency-Key, taskId, request_id) durch Logs und Ereignisse und halten Sie Prompts, API-Keys und private Asset-URLs aus Metrik-Labels und Routine-Logs heraus. Alarmieren Sie auf Symptome: „50.000 Einträge in der Queue“ kann normal sein; „der älteste interaktive Eintrag ist 12 Minuten alt“ ist handlungsrelevant.
Mit einem begrenzten Batch beginnen, dann Kapazität erhöhen
Erhöhen Sie die Kapazität nur, solange Alter des ältesten Eintrags, Task-Fehler und Ausgaben innerhalb Ihrer Schwellen bleiben; mehr Worker können einen überlasteten Abschlusspfad nicht beheben.
Unterbrechen Sie vor dem ersten bezahlten Batch mit einem Mock einen Worker unmittelbar vor und nach der Einreichung und lassen Sie 429, fehlgeschlagene Tasks und Ausgabenlimit-Fehler zurückgeben. Die Wiederherstellung sollte den gespeicherten Intent wiederverwenden, unbekannte Tasks ungeklärt lassen und neuen Dispatch stoppen, ohne den Abschluss zu stoppen. Fahren Sie dann einen kleinen freigegebenen Batch, bevor Sie die Zulassung für große Importe öffnen.
Zur Abschlussbehandlung siehe „Polling vs webhooks for async generation jobs“; für ein Batch-Budget siehe Per-unit pricing vs subscriptions. Die Produktions-Checkliste behandelt Key-Konfiguration und Launch-Tests.
FAQ
Hat SpicyAPI einen Batch-Endpunkt oder ein Parallelitätslimit?
Keinen Batch-Endpunkt: Jede Zeile ist ein createTask, und es gibt keine dokumentierte Obergrenze für Tasks in flight (die Limits stehen in der Tabelle oben; die aktuelle Rate steht in X-RateLimit-Limit). Ist das Rate-Limit für Ihre Spitze zu niedrig, bitten Sie den Console-Support um ein höheres und beschreiben Sie Spitzen-Requests pro Sekunde und Batch-Größe.
Kann ich einen versehentlich gestarteten Batch abbrechen?
Nein. Nutzen Sie den Dispatch-Kill-Switch, um neue Tasks zu stoppen, während die Verarbeitung der Abschlüsse weiterläuft.
Soll ein 429 einen Retry-Versuch verbrauchen?
Warten Sie Retry-After plus Jitter und behalten Sie denselben Idempotency-Key. Wiederholtes Drosseln verlangt eine niedrigere Dispatch-Rate. Begrenzen Sie Retries sowohl nach Versuchen als auch nach Zeit; ist die Annahme nach Ablauf dieses Budgets noch unsicher, gleichen Sie den gespeicherten Intent ab, bevor Sie einen Ersatz erzeugen.
Wie verhindere ich doppelte Abbuchungen nach einem Timeout?
Senden Sie innerhalb von 24 Stunden erneut mit demselben Idempotency-Key und demselben API-Key und speichern Sie die taskId, sobald Sie sie haben. Schlagen Sie nach 24 Stunden stattdessen die gespeicherte taskId nach.
Reicht eine Queue, um Überlast zu verhindern?
Nein. Eine Queue speichert Überlast; Backpressure steuert sie. Sie brauchen weiterhin Zulassungsschwellen auf das Queue-Alter, ein Limit für Tasks in flight und eine Produktantwort für den Fall, dass die zugesagte Wartezeit nicht zu halten ist.
Was ist eine gute Start-Parallelität?
Abschlüsse pro Sekunde mal mittlere Task-Dauer, je Medienklasse, plus etwa 25 % Spielraum: 40 bei 5 Bildern pro Sekunde zu je 8 Sekunden. Erhöhen Sie sie nur, solange Queue-Alter, Fehler und Ausgaben im grünen Bereich bleiben.
Können hochpriorisierte Jobs die Limits umgehen?
Nein. Die Priorität wählt unter zulässiger Arbeit aus und umgeht nie das globale Limit oder die Budgets. Geben Sie interaktiver Arbeit reservierte Kapazität und lassen Sie niedriger priorisierte Arbeit altern, damit sie nicht verhungert.
Einen Agent den Aufruf übernehmen lassen
Nutzen Sie einen Agent, wenn er die Eingaben vorbereiten und das Ergebnis zurückgeben soll, während Sie Preis und kreative Entscheidungen freigeben.
- CLI: Speichern Sie das
input-Objekt einer Zeile alsinput.json(für Z-Image:prompt,aspect_ratio,enable_prompt_expansion,output_format,seed) und nutzen Sienpx @spicyapi/cli tasks create --model alibaba/z-image-turbo/text-to-image --input-file input.json --wait. Die CLI bestätigt den Preis; behalten Sie die zurückgegebene Task-ID für die Wiederherstellung. - MCP: Bitten Sie Ihren Assistenten,
spicyapi_task_createfüralibaba/z-image-turbo/text-to-imagezu verwenden und danachspicyapi_task_wait. Der MCP-Server verlangt vor dem Erstellen eine Preisbestätigung. - Skill: Installieren Sie ihn mit
npx skills add spicyapi-ai/spicy-skillund bitten Sie dann$spicyapi, denselben Workflow auszuführen und für Ihre Freigabe anzuhalten. Der Skill nutzt eine konfigurierte CLI- oder MCP-Verbindung.








