Inteligência artificial, sem ruído.
Tutoriais11 min

Processe um Milhão de Documentos Durante a Noite: Batch Inference do Início ao Fim

Aprenda a processar 1 milhão de documentos com batch inference: pipeline completo, código testado e custo real de US$ 387,50 — metade do preço da API de tempo real.

Processe um Milhão de Documentos Durante a Noite: Batch Inference do Início ao Fim

O pipeline completo

Imagine que você tem um milhão de documentos em object storage e até amanhã de manhã precisa classificar cada um por categoria e gerar um resumo. O primeiro instinto — enviar um por um pela API de chat em tempo real — é o caminho errado. Com rate limits padrão, só os requests levam mais de 27 horas. Você paga o preço cheio por cada token. E uma falha de rede às 3 da manhã interrompe tudo.

Este artigo apresenta a alternativa correta: batch inference, o modelo de execução onde você empacota todos os milhões de requests em arquivos, entrega para a plataforma e coleta os resultados quando o job terminar. Com batch, o mesmo trabalho custa metade do preço e roda em capacidade isolada, sem consumir sua cota de tempo real ou degradar a latência das suas aplicações. Vamos construir o pipeline completo, com código testado e custos reais.

✅ O que você ganha

  • 50% de desconto em relação à API de tempo real — o mesmo trabalho que custaria US$ 775,00 sai por US$ 387,50
  • Execução isolada: batch jobs usam cota separada e não competem com tráfego de produção
  • Retry automático: a plataforma tenta de novo erros transitórios (429, 408, 5xx) até 2 vezes com backoff exponencial
  • Pipeline simples (~150 linhas de Python): você não escreve rate limiter, checkpoint, nem retry loop — a plataforma faz isso
  • Até 10 bilhões de tokens por modelo por conta como cota padrão de batch

⚠️ O que você NÃO ganha

  • Latência baixa: o job tem janela de conclusão de 24 horas e você não controla quando roda dentro dessa janela
  • Streaming: batch não suporta respostas em streaming, só processamento completo
  • Multi-modelo no mesmo job: cada batch job usa um único modelo (GPT-5 nano OU mini, não ambos no mesmo arquivo)
  • Modelos open-source: batch inference nas plataformas comerciais só suporta OpenAI e Anthropic; para Llama ou Qwen, você precisa de GPUs dedicadas

O job que vamos rodar

Para manter os números concretos, aqui está o projeto que o artigo da DigitalOcean modela:

  • 1.000.000 documentos em texto puro, ~1.200 tokens cada (~900 palavras)
  • Para cada documento: classificar em uma de 8 categorias + resumir em 3-4 frases
  • Prazo: resultados prontos na manhã seguinte
  • Modelo: GPT-5 mini (batch pricing: US$ 0,125/M tokens input, US$ 1,00/M tokens output)

Um detalhe de design importante: classificação e sumarização acontecem em um único request por documento, não dois. O prompt pede que o modelo retorne um JSON com category e summary juntos. Isso corta o número de requests pela metade e simplifica o pipeline.

Com as instruções do prompt, cada request carrega ~1.500 tokens de input e produz ~200 tokens de output. Para 1 milhão de documentos: 1,5 bilhão de tokens de input e 200 milhões de output.

Por que a API de tempo real é a ferramenta errada aqui

Os rate limits da API serverless de inferência no Tier 3-4 são de 600 requests por minuto e ~800K a 2M tokens por minuto. Um milhão de requests a 600/min leva 27,8 horas — isso assumindo zero retries e script perfeito. O limite de tokens é igualmente restritivo: 1,7 bilhão de tokens a 2M/min dá ~14 horas.

Você poderia contornar isso com tier increase, rate limiter customizado, checkpoint e sharding. Mas as equipes que fazem isso geralmente estão desperdiçando esforço: os rate limits não são um obstáculo, são um sinal de que a API de latência é o modelo de execução errado para um job de throughput.

Com batch inference, você não escreve rate limiter, retry loop com backoff, nem arquivo de checkpoint. A plataforma gerencia tudo: retry de erros transitórios, capacidade isolada, e falhas caem em um arquivo de erro ao invés de crashar seu script.

O trade-off é a velocidade: batch jobs têm janela de 24 horas. Se alguma parte do workload precisa de resposta em segundos, ela fica na API de tempo real. Tudo o resto é candidato a batch.

Planejando os limites

Batch inference tem três limites que determinam como você divide 1 milhão de documentos:

LimiteValor máximo
Requests por arquivo50.000
Tamanho por arquivo200 MB
Tokens submetidos por modelo/conta10 bilhões (padrão)

À primeira vista, 1.000.000 ÷ 50.000 = 20 arquivos. Mas cada arquivo também precisa ficar abaixo de 200 MB:

ItemTamanho
Documento (1.200 tokens × ~4 caracteres)~4.800 caracteres
Instruções do prompt (~300 tokens)~1.200 caracteres
JSON wrapper (custom_id, method, url, body)~500 caracteres
Total por linha~6.500 caracteres ≈ 6,5 KB

Com 6,5 KB por linha, o limite de tamanho de arquivo é atingido primeiro: 200 MB ÷ 6,5 KB ≈ 31.500 documentos por arquivo, bem abaixo do limite de 50.000 requests. A decisão final: 25.000 documentos por arquivo (~160 MB, seguro) → 40 arquivos → 40 batch jobs. O total de tokens (1,7 bilhão) está confortavelmente abaixo do limite de 10 bilhões.

Construindo os arquivos de input

Cada linha do arquivo de batch (JSONL) é um request autocontido no formato da Batch API da OpenAI. Três detalhes que fazem diferença na prática:

  1. custom_id é sua âncora: use o ID real do documento, nunca um índice de array. Resultados não voltam na ordem de input. IDs duplicados no mesmo arquivo falham na validação.
  2. Limite o output: 500 tokens é suficiente para resumos de 3-4 frases. Com GPT-5, use max_completion_tokens (não o antigo max_tokens) e reasoning_effort: "minimal" para evitar gastar tokens de raciocínio em uma tarefa simples.
  3. Valide localmente antes de subir: um parser de 10 linhas que verifica JSON válido e unicidade de custom_id evita um ciclo inteiro de upload desperdiçado.
import json

SYSTEM_PROMPT = (
    "You classify and summarize documents. Respond with a single JSON object: "
    '{"category": "<one of: billing, bug_report, feature_request, account, '
    'security, performance, documentation, other>", '
    '"summary": "<3-4 sentence summary>"} '
    "The category value must be exactly one of the eight listed strings, "
    "lowercase. Never invent another category; if unsure, use \"other\"."
)

CHUNK_SIZE = 25_000

def write_batch_files(documents, prefix="batch_input"):
    """documents yields (doc_id, text) tuples. Returns list of file paths."""
    paths, out, count, part = [], None, 0, 0
    for doc_id, text in documents:
        if count % CHUNK_SIZE == 0:
            if out:
                out.close()
            part += 1
            path = f"{prefix}_{part:03d}.jsonl"
            out = open(path, "w", encoding="utf-8")
            paths.append(path)
        line = {
            "custom_id": doc_id,
            "method": "POST",
            "url": "/v1/chat/completions",
            "body": {
                "model": "gpt-5-mini",
                "messages": [
                    {"role": "system", "content": SYSTEM_PROMPT},
                    {"role": "user", "content": text},
                ],
                "max_completion_tokens": 500,
                "reasoning_effort": "minimal",
            },
        }
        out.write(json.dumps(line, ensure_ascii=False) + "\n")
        count += 1
    if out:
        out.close()
    return paths

Upload e criação dos jobs

O upload segue três passos por arquivo, e a ordem importa:

  1. POST /v1/batches/files: reserva um file_id e uma URL de upload pré-assinada (válida por ~15 minutos)
  2. PUT do JSONL bruto na URL pré-assinada com Content-Type: application/octet-stream
  3. Criar o batch job com o file_id — esta etapa verifica o storage; se o upload não terminou, falha

O request_id é uma chave de segurança que previne jobs duplicados: derive-o do nome do arquivo (ex: hash MD5), não de um UUID aleatório. Se o script de submissão der erro de rede e retentar, o mesmo request_id retorna o job existente em vez de criar um duplicado que cobraria 25.000 documentos duas vezes.

Monitorando 40 jobs

Cada job passa por estados fixos: validatingqueuedin_progress → terminal (completed, failed, expired, cancelled).

Importante: status completed significa que todos os requests foram processados, mesmo que alguns individuais tenham falhado — essas falhas vão para o arquivo de erro, não para o status. Jobs expired ou cancelled não são perda total: tudo que completou antes é preservado, baixável e cobrado. Apenas requests não processados são descartados, e você não é cobrado por eles.

Polling a cada 60 segundos é suficiente para um job overnight. A API de batch não envia notificação — o loop de polling é como você descobre que terminou:

import time

def wait_for_jobs(batch_ids, poll_seconds=60):
    pending = set(batch_ids.values())
    terminal = {"completed", "failed", "expired", "cancelled"}
    states = {}
    while pending:
        for bid in list(pending):
            b = client.batches.retrieve(bid)
            status = b["status"]
            counts = b.get("request_counts", {})
            print(f"{bid}  {status:12}  "
                  f"{counts.get('completed', 0)}/{counts.get('total', 0)}")
            if status in terminal:
                states[bid] = status
                pending.discard(bid)
        if pending:
            time.sleep(poll_seconds)
    return states

Lidando com falhas

Falhas acontecem em três níveis:

  • Request-level: documentos que excedem a janela de contexto, violações de política de conteúdo, prompts mal formatados. Não falham o job — vão para o arquivo de erro com custom_id e código de erro. Resolva truncando, revisando ou reenviando como um batch job final separado.
  • Job-level: raras. Se um job expirar, crie um job de continuação processando apenas os requests pendentes.
  • Validação de output: o request pode retornar JSON inválido ou uma categoria fora da lista. Os testes do artigo mediram isso: GPT-5 nano inventou categoria fora da lista em 8% dos casos (4 de 50). GPT-5 mini: 4% (2 de 50) antes do prompt tightening, 0% depois. Moral: valide os valores do JSON, não apenas a estrutura.

Recuperando e juntando resultados

Quando o job atinge estado terminal, GET /v1/batches/{batch_id}/results retorna URLs pré-assinadas para o arquivo de output (e de erro, se houver). Baixe imediatamente — as URLs expiram rápido e os arquivos são retidos por apenas 30 dias.

Cada linha de output carrega custom_id, a resposta completa da API (incluindo tokens usados por request) e um campo error nulo em caso de sucesso. O join de volta aos documentos fonte é um lookup de dicionário — e somar os campos usage conforme processa dá a contagem exata de tokens para conferir com a conta.

A conta detalhada

Tokens batch são cobrados com até 50% de desconto sobre os preços de tempo real. Não há taxa separada para upload de arquivo, armazenamento, criação de job ou polling. A conta do job modelado:

ItemQuantidadePreçoCusto
Input tokens (1M docs × ~1.500)1,5B tokensUS$ 0,125 / 1MUS$ 187,50
Output tokens (1M docs × ~200)200M tokensUS$ 1,00 / 1MUS$ 200,00
Upload de arquivos (40)40US$ 0US$ 0,00
Criação e polling (40 jobs)40US$ 0US$ 0,00
TotalUS$ 387,50

Isso dá ~US$ 0,0004 por documento. O mesmo workload em tempo real: US$ 375,00 (input) + US$ 400,00 (output) = US$ 775,00. A diferença de US$ 387,50 é o preço da urgência — pagar por respostas em segundos quando o deadline real é amanhã de manhã.

ModeloBatch input/output por 1M tokensTotal do job
GPT-5 nanoUS$ 0,025 / US$ 0,20US$ 77,50
GPT-5 miniUS$ 0,125 / US$ 1,00US$ 387,50
Claude Haiku 4.5US$ 0,50 / US$ 2,50US$ 1.250,00

Para classificação simples, GPT-5 nano a US$ 77,50 o milhão é a escolha racional — com um passe de validação e retry para as categorias inválidas (~8% de re-processamento, adicionando ~8% à conta, ainda ~5× mais barato que mini). Se os resumos alimentam algo que um humano vai ler, pague pelo mini. Uma avaliação com 50 documentos de teste nas duas opções custou menos de US$ 1 e resolveu uma decisão de quatro dígitos com medições em vez de instinto.

Quando usar batch, tempo real ou self-hosting

Batch é a ferramenta certa quando:

  • O deadline é uma hora do dia, não um número de segundos
  • O volume de requests é grande (>10.000)
  • Você não precisa de streaming, extended thinking ou features específicas de provider
  • O custo por request importa mais que a latência

Fique no tempo real quando: latência importa (alguém está esperando), o volume é pequeno, ou você precisa de features que batch não suporta.

Self-hosting em GPUs dedicadas (ex: H100 a US$ 4,41/hora na DigitalOcean) começa a vencer em três cenários: (1) modelos open-source como Llama ou Qwen que batch inference não suporta; (2) uso constante e pesado onde GPU dedicada é mais barata que per-token; (3) quando documentos não podem sair da sua infraestrutura. O custo extra do self-hosting é engenharia: serving stack, batching logic, monitoring, retries — tudo que a Batch API já faz para você.

Troubleshooting

  • ❌ Sintoma: job falha na validação com erro genérico. Causa: linha JSONL quebrada ou custom_id duplicado. Solução: valide localmente com um script que parseia cada linha e verifica unicidade de IDs antes do upload.
  • ❌ Sintoma: PUT na URL pré-assinada retorna 403. Causa: URL expirou (válida por ~15 min) ou Content-Type inválido. Solução: solicite nova intent de arquivo, use application/octet-stream.
  • ❌ Sintoma: job criado, mas nunca sai de queued. Causa: saldo pré-pago zerado. Batch inference é pré-pago; se o saldo chegar a zero, o acesso é suspenso. Solução: verifique e recarregue o saldo.
  • ❌ Sintoma: output contém JSON inválido ou categoria fora da lista. Causa: o modelo alucinou uma categoria ou retornou texto livre. Solução: adicione validação pós-processamento; reenvie esses custom_id em um batch de limpeza separado.
  • ❌ Sintoma: custo maior que o esperado. Causa:max_completion_tokens não configurado ou reasoning_effort não definido como minimal, resultando em tokens de raciocínio cobrados. Solução: configure ambos explicitamente no body de cada request.

FAQ

Batch inference funciona com qualquer modelo? Não. Na DigitalOcean, apenas modelos comerciais OpenAI e Anthropic com prompts de texto são suportados. Modelos open-weight (Llama, Qwen) precisam de GPUs dedicadas ou pricing per-token de open-source.

Posso cancelar um job depois de criado? Sim. Requests já completados são preservados e cobrados; os pendentes são descartados sem custo.

Quanto tempo os resultados ficam disponíveis? Até 30 dias após a conclusão do job. Baixe e arquive em storage próprio como parte do pipeline, não como depois.

Batch funciona com múltiplos modelos no mesmo arquivo? Não. Cada batch job usa um único modelo. Se quiser mandar documentos fáceis para GPT-5 nano e difíceis para mini, são dois batch jobs separados.

Como evito jobs duplicados se meu script der erro de rede? Use request_id determinístico (ex: hash do nome do arquivo) em vez de UUID aleatório. Reexecutar o script com o mesmo request_id retorna o job existente.

Qual a diferença entre batch e Inference Router? O Inference Router da DigitalOcean seleciona o melhor modelo por request em tempo real. Isso não se aplica a batch jobs — batch inference requer que você especifique um modelo por job.


Leitura estimada: 8 minutos


Descubra mais sobre noticiAI

Assine para receber nossas notícias mais recentes por e-mail.

R
Sobre o autorRedação Noticiai

Equipe editorial dedicada a explicar inteligência artificial com clareza, independência e contexto.