Ilustração: filas separadas de imagens e vídeos deixam cada carga avançar no próprio ritmo.
Um gerador em lote costuma falhar no momento menos interessante: não ao gerar uma imagem, mas quando um cliente importa 20.000 linhas e a aplicação transforma todas elas em requisições ativas de uma só vez.
A API pode estar saudável e cada payload ser válido. Mesmo assim o sistema colapsa: as conexões com o banco de dados se esgotam, os callbacks chegam mais rápido do que você consegue fazer o commit, um tenant grande bloqueia todos os outros, uma retentativa duplica um vídeo caro e um pico de tráfego vira uma fatura.
Mantenha o trabalho em espera em uma fila durável, limite as tarefas em andamento e só tente de novo quando souber se já existe uma tarefa remota. Um Promise.all maior não faz nada disso.
Um fato molda tudo o que vem a seguir: a SpicyAPI não tem endpoint em massa nem em lote. Cada linha é uma chamada createTask com a própria tarefa, o próprio hold e a própria liquidação, e uma tarefa aceita não pode ser cancelada. A fila, o limite de concorrência e o botão de parar ficam, portanto, na sua aplicação.
Experimente primeiro no Playground
Abra o Playground do z-image. Escreva um Prompt representativo, escolha a Aspect ratio, defina Prompt expansion como você pretende rodar o lote, clique em Run model e confira o Output. Só aumente a escala quando esse único resultado for o que você quer; um agendador multiplica a requisição, inclusive os erros dela.
A resposta curta
- Mantenha o backlog na sua própria fila durável, não em uma requisição HTTP nem em uma promise em memória.
- Limite você mesmo as tarefas em andamento. O limite de taxa da plataforma (1.000 requisições a cada 10 segundos, separadamente para leituras e escritas) não limita quantas tarefas aceitas rodam ao mesmo tempo; quem limita são o seu saldo e os seus tetos de gasto. Escolha um número, por classe de mídia, e mantenha-o até a tarefa ser terminal.
- Admita pela idade da fila, não pelo tamanho, e decida o custo do lote inteiro antes do primeiro dispatch.
- Uma chave de idempotência salva por saída lógica, sempre enviada com a mesma API key e nunca considerada confiável depois de 24 horas.
- Tente de novo conforme o código de erro: erros de capacidade e de transporte, sim; entrada inválida, saldo vazio e tetos de gasto, não.
- Round-robin entre tenants, e uma chave de emergência do dispatch que deixe o tratamento das conclusões funcionando.
Três limites e quem define cada um
Trate envio, dispatch e conclusão como etapas separadas:
lote do cliente
│
▼
admissão + checagem de custo ──► fila durável ──► workers limitados ──► createTask
│
conclusão por polling / webhook
│
copiar saída ──► armazenamento de resultados + evento ao cliente
| Limite | O que ele restringe | Quem define na SpicyAPI |
|---|---|---|
| Admissão | Quanto backlog você mantém e promete concluir | Você |
| Taxa de envio | Chamadas createTask por segundo | Plataforma: 1.000 escritas a cada 10 segundos por conta, um token bucket de cerca de 100 requisições por segundo, com X-RateLimit-Remaining em toda resposta e Retry-After em um 429 |
| Tarefas em andamento | Tarefas aceitas e não concluídas | Principalmente você. A plataforma só as limita pelos fundos que cada tarefa retém, pelos tetos de gasto da chave, da equipe e da plataforma (40202) e pela capacidade do modelo (50301) |
O limite de taxa governa o envio, não a concorrência: tarefas aceitas não continuam consumindo o bucket de requisições enquanto geram.
Com 5 conclusões por segundo, você cria cerca de 5 tarefas por segundo, uma pequena fração do bucket de 100 por segundo. Para a maioria dos lotes, o limite de envio é um fusível de segurança, não o seu acelerador; o dinheiro retido pelas tarefas em andamento e os seus próprios tetos param você primeiro. O polling usa o bucket de leitura separado, então um loop de polling descontrolado não consegue bloquear createTask, mas pode começar a retornar 429 em recordInfo e deixar você às cegas. Leia o cabeçalho em vez de fixar os valores padrão no código.
Outras APIs podem impor limites separados de concorrência e de fila pendente; o guia de concorrência da Leonardo os distingue. A SpicyAPI não tem um teto numérico documentado para tarefas em andamento, então configure o seu em vez de depender do limite de taxa de requisições.
Dimensione o teto em andamento com tempo e dinheiro
Para uma classe de carga estável, a Lei de Little relaciona o número médio de tarefas no sistema com a taxa de conclusão e o tempo médio que cada tarefa passa nele:
tarefas em andamento médias ≈ conclusões por segundo × segundos médios da aceitação ao estado terminal
Se as tarefas de imagem levam em média 8 segundos e você precisa de 5 por segundo:
5 jobs/s × 8 s = 40 tarefas de imagem simultâneas
Trate 40 como ocupação média, não como garantia. Um teto inicial de 50 acrescenta 25 % de folga; aumente-o apenas enquanto idade da fila, erros e gasto continuarem saudáveis. Use p95 e p99 para as caudas e para os alarmes de idade da fila. Meça o tempo da tarefa a partir de createdAt e completedAt em recordInfo, por modelo e configuração.
Vídeo merece o próprio pool. Se os clipes do Seedance 2.0 levam em média 70 segundos na sua carga e você quer 0,2 por segundo:
0,2 job/s × 70 s = 14 tarefas de vídeo simultâneas
As médias de 8 e 70 segundos são suposições de dimensionamento, não tempos de geração prometidos.
Compartilhando um único pool de 50, uma rajada de vídeos longos pode ocupar todos os slots enquanto imagens rápidas esperam. Com 50 slots de imagem e 14 de vídeo, as duas classes continuam andando. Z-Image e Seedream 5.0 Pro também podem ter pools separados se as durações ou prioridades forem diferentes.
Agora confira o teto contra o dinheiro. Cada tarefa aceita retém o seu estimatedCost até ser liquidada, e esse hold conta para o teto de cada chave e equipe. Com preços públicos do catálogo:
| Em andamento | Retido ao mesmo tempo |
|---|---|
| 50 imagens Seedream 5.0 Pro em 1K ($0,036 cada) | $1,80 |
| 14 clipes de texto para vídeo Seedance 2.0, 5 s em 720p ($0,152 por segundo, $0,76 cada) | $10,64 |
Um saldo que não cubra esses holds faz as novas tarefas falharem com 40201, por melhor que seja o seu agendador. O gasto total importa mais: 20.000 imagens Seedream 5.0 Pro em 1K somam $720 e 20.000 imagens Z-Image a $0,01 com Prompt expansion desativado, $200. Os preços mudam, então leia-os na página do modelo ao vivo ou em uma cotação. O teto que impede um lote ruim é o que você define antes de ele começar: defina um teto de gasto diário na chave de produção antes do dispatch, dimensionado pelo máximo que você pode perder em um dia ruim. Um teto em branco significa sem limite; os limites da equipe também podem barrar novas tarefas com 40202.
A estimativa do tempo de escoamento é a outra metade:
tempo de escoamento ≈ jobs na fila ÷ conclusões sustentáveis por segundo
18.000 imagens na fila a 5 por segundo são uma hora de trabalho. Se você promete começar em 15 minutos, a admissão precisa desacelerar bem antes de 18.000. Um backlog que cresce não é regime estável, então confie mais na idade da fila observada do que nas equações.
O backpressure começa antes do dispatch
Backpressure significa que o sistema a jusante consegue fazer o produtor desacelerar. Sem ele, a fila absorve trabalho para sempre e o cliente só descobre a sobrecarga horas depois.
Use dois limiares de admissão sobre a idade da fila, a idade do item pronto mais antigo. O tamanho engana quando os itens custam valores diferentes; a idade diz diretamente se a espera prometida está sendo cumprida.
- Limiar brando: aceitar o lote, devolver uma estimativa atrasada, desacelerar o produtor.
- Limiar rígido: adiar ou rejeitar o trabalho novo com uma resposta de produto repetível antes que exista qualquer tarefa remota.
Compare dinheiro em unidades decimais exatas, não em ponto flutuante binário. Faça o commit da reserva de forma atômica junto com a admissão do lote no seu adaptador de fila, para que envios concorrentes não consumam o mesmo orçamento restante.
Não transforme toda sobrecarga interna em um 429 de API para os seus próprios clientes. Se o trabalho está armazenado de forma durável, devolva um ID de lote aceito em estado adiado. Rejeite apenas o trabalho que você não consegue manter com responsabilidade e, ao fazê-lo, informe um horário para tentar de novo e torne idempotente o próprio envio do lote.
Dois detalhes desta API decidem por quanto tempo um item pode esperar com segurança na sua fila:
- Arquivos de referência expiram em um dia. Um URI
spicy://de um upload é válido até oexpiresAtda sua resposta de commit (um dia por padrão), e uma retentativa reutiliza a entrada original, então uma retentativa depois disso retorna400. Guarde na fila uma URL HTTPS pública ou o seu próprio ID de origem e faça o upload no momento do dispatch. - Cotações duram cinco minutos. Se você quer travar o preço, cote no dispatch e envie
quoteIdcomexpectedCostparacreateTask; uma mudança de preço então falha com40901em vez de cobrar um valor novo. Cotações são gratuitas, mas contam contra o bucket de leitura, então cotar 20.000 linhas na admissão é o lugar errado.
A idempotência tem duas camadas
Dois duplicados diferentes precisam ser evitados: o cliente enviando o mesmo lote duas vezes e o seu dispatcher sem saber se uma criação teve sucesso. Dê ao lote uma identidade estável e depois gere e salve um UUID para cada novo item lógico. A recuperação reutiliza a chave salva desse item.
Mantenha uma restrição de unicidade nessa chave no seu banco de dados. Grave o item como «intenção» antes de chamar a API, envie a chave como Idempotency-Key e armazene o taskId retornado na mesma linha. Depois de uma queda, o próximo worker reutiliza a chave salva se a aceitação ainda for desconhecida. Gravar a intenção só depois de enviar deixa uma janela em que uma reinicialização pode comprar a mesma saída duas vezes.
O que a documentação de idempotência acrescenta e a fila precisa respeitar:
- Uma chave vale por 24 horas a partir da primeira requisição aceita; replays não a prolongam. Depois disso, a mesma chave cria uma nova tarefa cobrada. Antes de reproduzir um item antigo da fila ou da dead-letter, consulte o
taskIdarmazenado em vez de confiar na chave. - A chave pertence à mesma conta e à mesma API key, e ao modelo,
inputecallBackUrlexatos. Mudar qualquer coisa retorna409. Uma chave irmã da sua conta também recebe409e não consegue ler a tarefa: só a chave que criou uma tarefa pode lê-la ou tentá-la de novo, então use uma chave só para criar, consultar e repetir em uma frota de workers. - Um replay retorna a tarefa original no estado atual, sem marcação de que foi um replay.
- Uma rara corrida com a mesma chave retorna
409«Idempotency key conflict, please retry shortly». Tente de novo uma vez após algumas centenas de milissegundos; as outras duas mensagens409nunca terão sucesso.
Um dispatcher que mantém o teto até a tarefa terminar
Salve um objeto por linha em batch.jsonl, por exemplo {"model": "alibaba/z-image-turbo/text-to-image", "input": {"prompt": "<one row's prompt>"}}, usando as entradas aceitas pelo modelo escolhido. Defina SPICY_API_KEY, BATCH_BUDGET_USD e, opcionalmente, BATCH_WORKERS (padrão 2). Execute este exemplo em Python e aprove o teto do lote. Ele mantém cada worker ocupado até a sua tarefa ser terminal, liquidada e não estar mais preparando a saída.
Guarde a pasta batch-run para a recuperação e não reordene as linhas de origem dela. A mesma pasta preserva o gasto liquidado e as reservas entre reinicializações. Execute apenas um coordenador por pasta.
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)))A documentação de integração em produção cobre a implantação. Este exemplo isola linhas inválidas, pausa a nova admissão por saldo ou erros incertos e limita retentativas transitórias. Tarefas já aceitas continuam sendo coletadas depois que a admissão pausa. Ele deliberadamente não faz retentativa paga automática; uma linha com falha permanece registrada para uma nova tentativa explícita após a liquidação. Rode um processo desses por classe de mídia e coloque o round-robin por tenant na frente do arquivo.
Agendamento justo: proteja os tenants pequenos
Primeiro a entrar, primeiro a sair é justo com os jobs, não com os clientes. Se o tenant A enfileira 100.000 itens por minuto antes de o tenant B enfileirar dez, B espera atrás de todos os de A. Mantenha uma fila por tenant e pegue um item por tenant a cada rodada:
rodada 1: A1, B1, C1
rodada 2: A2, B2, C2
Os pesos podem refletir a capacidade contratada, mas dê a cada tenant ativo uma cota mínima. Limite também as tarefas em andamento de cada tenant: o teto global protege a sua conta, o do tenant contém os vizinhos barulhentos. Dentro de um tenant, faixas de prioridade (prévias interativas, lotes normais, backfills) só valem a pena quando a latência das prévias for uma reclamação real. Se você as adotar, faça a prioridade envelhecer com o tempo de fila; caso contrário, um fluxo constante de prévias deixa um backfill sem atendimento para sempre. Tudo em uma conta compartilha os mesmos dois buckets de requisições e o mesmo saldo, então a justiça entre tenants é trabalho seu, não da API.
Classifique as falhas antes de tentar de novo
«Tentar três vezes» não é uma política. A ação certa depende do que você sabe e de uma tarefa já poder existir. Esta tabela usa os códigos documentados; os valores de code estão no corpo da resposta, e as falhas de tarefa chegam como state: "failed" com um errorCode em uma chamada recordInfo bem-sucedida (code: 200).
| Onde | Sinal | Retentar? | O que fazer |
|---|---|---|---|
| Criar | Timeout, conexão interrompida, corpo ilegível | Sim | Resultado desconhecido. Mesma chave, mesma API key, backoff com jitter |
| Criar | 429 com Retry-After | Sim | Espere os segundos indicados, some jitter, reduza a taxa de dispatch. Não interprete como prova de que você estava rápido demais |
| Upload | 429 sem Retry-After (uploads pendentes) | Sem retentativa imediata | Conclua os uploads pendentes ou espere os tickets deles expirarem antes de pedir mais |
| Criar | 500, 503, 50301 (modelo indisponível) | Sim, com limite | Backoff com prazo; 50301 também pode significar ausência de preço ativo, então atualize o catálogo |
| Criar | 409 corrida com a mesma chave | Uma vez | Algumas centenas de ms, mesma chave. Outros 409 significam payload alterado ou outra chave: pare |
| Criar | 400, 40003, 40004 | Não | Campo inválido, combinação não suportada ou tipo/tamanho de arquivo incompatível; corrija o item e mostre a mensagem |
| Criar | 40201 saldo, 40202 teto de gasto | Não | Pause o dispatch e alerte. Tentar de novo não muda um saldo nem um teto |
| Criar | 401, 403, 40301 | Não | Permissão de chave ou de modelo: pause a integração e alerte um operador |
| Tarefa | rate_limited, upstream_unavailable, upstream_failed | Sim | Só depois de settled: true: POST /api/v1/jobs/retry com uma chave ligada à tarefa de origem; cria uma nova tarefa cobrada. Uma retentativa antes disso retorna 409 |
| Tarefa | timeout, generation_failed | Talvez | Costuma falhar do mesmo jeito. Tente uma duração menor ou uma resolução mais baixa, ou desista |
| Tarefa | invalid_request, unsupported_combination, content_rejected | Não | A mesma entrada falha de novo. Mude a entrada ou escolha outro modelo |
| Tarefa | invalid_asset | Nova revisão | O arquivo de referência sumiu: faça upload de novo, atualize a entrada e cote uma nova tarefa |
| Local | Callback recebido, a sua transação falhou | Sim | Repita o seu handler, não a criação; deduplique por ID de entrega ou taskId |
Use backoff aleatório para que os workers não repitam em sincronia.
Mova as falhas confirmadas e as entradas rejeitadas para uma dead-letter queue com a classificação e a ação de replay segura. Mantenha uma criação ambígua ou uma tarefa aceita não resolvida na fila de reconciliação com a chave salva e o ID da tarefa; um prazo de retentativa local não prova que ela falhou. Uma tarefa com falha normalmente libera o hold por completo, mas alguns resultados content_rejected cobráveis são liquidados na cotação, então leia settled e cost em vez de presumir reembolso.
A proteção de custos pertence ao agendador
Limites de concorrência protegem máquinas, não dinheiro. Um worker perfeitamente estável ainda pode fazer o dispatch de um lote impagável. Confira o custo estimado contra cada envelope antes do dispatch:
permitido = restante diário do tenant
∩ restante mensal do tenant
∩ restante do lote do projeto
∩ restante global por hora
Reconcilie com a liquidação real: createTask retorna o estimatedCost retido, uma tarefa bem-sucedida é liquidada nesse valor ou abaixo dele, e settled: true com cost é a cobrança final. Libere a sua própria reserva dos itens que a API rejeitou e mantenha a reserva dos aceitos até settled virar. Não adivinhe a partir de um timeout HTTP.
Proteções úteis:
- um custo estimado máximo por lote, mostrado a uma pessoa antes de um grande começar;
- um orçamento de dispatch por hora por tenant e um alarme de velocidade de gasto global;
- um circuit breaker por modelo ou tenant;
- tetos de API key abaixo do saldo da conta e um alerta de saldo baixo;
- uma chave de emergência que pare o novo dispatch e deixe o processamento das conclusões funcionando.
Como as tarefas aceitas não podem ser canceladas, parar a criação é o único freio: o teto de tarefas em andamento, e não um botão de cancelar, limita o dano em um incidente de custo. Continue recebendo webhooks, consultando tarefas antigas, liquidando reservas e copiando saídas.
Conclusão em escala de lote
A entrega por callBackUrl de cada tarefa expira em 15 segundos; um endpoint de conta registrado também pode receber a própria entrega, então o receptor deve verificar a assinatura contra o corpo bruto, gravar o evento em uma tabela de inbox, retornar 2xx e deixar um worker fazer o resto. Deduplique pelo request_id de entrega v2 e decida por data.state, não pela ordem de chegada. Mantenha um polling lento como reserva para eventos perdidos (veja a documentação de webhooks).
Copie as saídas para o seu próprio armazenamento à medida que as tarefas terminam. Os links de resultado duram cerca de 20 minutos e a mídia gerada é mantida por cerca de 14 dias no máximo (retenção); a 5 conclusões por segundo, a etapa de cópia faz parte do seu throughput, e um uploader lento vira o gargalo que se esconde em tarefas «concluídas». Encurtar a retenção também pode remover resultados de tarefas que você já executou.
Observe o fluxo, não só os erros
Um bom painel responde a quatro perguntas: estamos aceitando trabalho, ele está começando, está terminando, o custo está sendo liquidado como esperado? Acompanhe por modelo, classe de mídia, nível de tenant e resultado:
| Sinal | Por que importa |
|---|---|
| Idade do item pronto mais antigo | Medida direta da espera do cliente e da falha de backpressure |
| Contagens na fila, em andamento e terminais | Mostra onde o trabalho se acumula |
| Tarefas aceitas por segundo vs. criações tentadas | Separa o throughput real das retentativas |
Duração da tarefa p50/p95/p99 (completedAt - createdAt) | Orienta o dimensionamento da concorrência e a detecção de tarefas travadas |
Resultados terminais e mix de errorCode | Degradação específica de modelo ou de entrada |
X-RateLimit-Remaining | Avisa antes de um 429 |
Saldo retido vs. disponível (GET /api/v1/chat/credit) | Prevê 40201 antes que ele pare o lote |
| Custo reservado vs. liquidado, gasto por hora por tenant | Reservas vazadas, lotes descontrolados |
| Atraso de webhooks e recuperações por polling | Saúde do caminho de conclusão |
Carregue uma única cadeia de correlação (batchId, ID do item, chave de idempotência, taskId, request_id) por logs e eventos, e mantenha prompts, API keys e URLs de assets privados fora dos rótulos de métricas e dos logs de rotina. Alerte sobre sintomas: «50.000 itens na fila» pode ser normal; «o item interativo mais antigo tem 12 minutos» pede ação.
Comece com um lote limitado e depois aumente a capacidade
Aumente a capacidade apenas enquanto a idade do item mais antigo, as falhas de tarefa e o gasto ficarem dentro dos seus limiares; mais workers não consertam um caminho de conclusão sobrecarregado.
Antes do primeiro lote pago, use um mock para interromper um worker imediatamente antes e depois do envio e para retornar 429, tarefas com falha e erros de teto de gasto. A recuperação deve reutilizar a intenção salva, manter as tarefas desconhecidas sem resolução e parar o novo dispatch sem parar a conclusão. Depois rode um pequeno lote aprovado antes de abrir a admissão para importações grandes.
Para o tratamento de conclusões, veja «Polling vs webhooks for async generation jobs»; para o orçamento de um lote, veja Per-unit pricing vs subscriptions. O checklist de produção cobre a configuração de chaves e os testes de lançamento.
FAQ
A SpicyAPI tem endpoint em lote ou limite de concorrência?
Não há endpoint em lote: cada linha é um createTask, e não existe teto documentado para tarefas em andamento (os limites estão na tabela acima; a taxa atual está em X-RateLimit-Limit). Se o limite de taxa for baixo demais para o seu pico, peça um maior ao suporte do console, descrevendo o pico de requisições por segundo e o tamanho do lote.
Posso cancelar um lote iniciado por engano?
Não. Use a chave de emergência do dispatch para parar novas tarefas enquanto o tratamento das conclusões continua.
Um 429 deve consumir uma tentativa de retentativa?
Espere Retry-After mais jitter e mantenha a mesma chave de idempotência. Limitação repetida pede uma taxa de dispatch menor. Limite as retentativas por tentativas e por tempo; se a aceitação ainda for incerta quando esse orçamento acabar, reconcilie a intenção salva antes de criar um substituto.
Como evito cobranças duplicadas após um timeout?
Reenvie com a mesma Idempotency-Key e a mesma API key em até 24 horas, e armazene o taskId assim que tiver. Depois de 24 horas, consulte o taskId armazenado.
Uma fila basta para evitar sobrecarga?
Não. Uma fila armazena a sobrecarga; o backpressure a controla. Você ainda precisa de limiares de admissão sobre a idade da fila, de um teto de tarefas em andamento e de uma resposta de produto para quando a espera prometida não puder ser cumprida.
Qual é uma boa concorrência inicial?
Conclusões por segundo vezes o tempo médio da tarefa, por classe de mídia, mais cerca de 25 % de folga: 40 para 5 imagens por segundo a 8 segundos cada. Aumente apenas enquanto idade da fila, erros e gasto continuarem saudáveis.
Jobs de alta prioridade podem contornar os limites?
Não. A prioridade escolhe entre o trabalho admissível e nunca contorna o teto global nem os orçamentos. Dê ao trabalho interativo uma capacidade reservada e faça o de menor prioridade envelhecer para não ficar sem atendimento.
Deixe um Agent cuidar da chamada
Use um Agent quando quiser que ele prepare as entradas e devolva o resultado enquanto você aprova o preço e as escolhas criativas.
- CLI: com o objeto
inputde uma linha salvo comoinput.json(para Z-Image:prompt,aspect_ratio,enable_prompt_expansion,output_format,seed), usenpx @spicyapi/cli tasks create --model alibaba/z-image-turbo/text-to-image --input-file input.json --wait. A CLI confirma o preço; guarde o ID de tarefa retornado para a recuperação. - MCP: peça ao seu assistente para usar
spicyapi_task_createcomalibaba/z-image-turbo/text-to-imagee depoisspicyapi_task_wait. O servidor MCP pede confirmação do preço antes da criação. - Skill: instale com
npx skills add spicyapi-ai/spicy-skille depois peça ao$spicyapique execute o mesmo workflow e pare para a sua aprovação. A Skill usa uma conexão CLI ou MCP configurada.








