Desenvolvimento

Queueable, Batch ou Schedulable: o async certo para cada trabalho

Ilustração em linha de três fluxos assíncronos paralelos convergindo numa fila de processamento

Todo dev Salesforce chega no momento em que o síncrono não dá mais conta: o callout precisa sair fora do trigger, a recalculada de carteira não cabe nos 100 SOQL da transação, o relatório tem que rodar às duas da manhã. Aí vem a pergunta que separa código que escala de código que estoura em produção na primeira carga grande: qual dos três modelos de assíncrono usar. A escolha errada não dá erro no sandbox com dez registros. Ela dá erro com dois milhões.

Use Queueable para trabalho assíncrono curto e encadeável, Batch para percorrer milhões de registros em blocos sem estourar limite, e Schedulable para disparar algo em horário fixo. Future é o modelo legado que Queueable substitui em código novo. Quando precisa agendar um processamento em massa, combine os dois: um Schedulable que enfileira um Batch.

O erro que custa caro: escolher async no chute

Em code review de org que já rodou algumas cargas grandes, o padrão que mais encontro não é a falta de assíncrono. É o assíncrono errado. O caso clássico: um @future que recalcula o valor de todas as oportunidades de uma conta. Funciona no sandbox, funciona com a conta que tem oito oportunidades, e quebra no dia em que importam uma conta com quinze mil. O método future roda tudo numa transação só, bate no limite de 50 mil registros de DML ou nos 100 SOQL, e a carga inteira falha sem ninguém saber por quê.

A raiz do problema é tratar "assíncrono" como uma coisa só. São três modelos, e cada um resolve um eixo diferente:

  • Encadeamento e desacoplamento. Preciso rodar algo fora da transação atual, talvez em sequência, passando dados ricos. Esse é o território do Queueable.
  • Volume. Preciso percorrer mais registros do que cabem nos limites de uma transação. Esse é o território do Batch.
  • Tempo. Preciso disparar em um horário, não em resposta a um evento. Esse é o território do Schedulable.

Quem escolhe pelo eixo errado paga em produção. Vamos aos três, do mais simples ao que combina todos.

Future: o legado que você mantém, não escreve

O método @future foi a primeira forma de assíncrono no Apex, e ainda aparece em toda org com alguns anos. Ele serve para tirar trabalho da transação síncrona, tipicamente um callout que não pode rodar dentro de um trigger:

public class ContaService {
    @future(callout=true)
    public static void sincronizarComErp(Set<Id> contaIds) {
        // callout para o ERP, fora da transacao do trigger
    }
}

O problema do future não é o que ele faz, é o que ele não deixa fazer. Os parâmetros só podem ser tipos primitivos ou coleções de primitivos: você passa um Set<Id>, nunca uma List<Account>. Ele não devolve nada, então não há como monitorar se o job terminou ou falhou sem ir caçar na tabela AsyncApexJob. E, o mais limitante, um future não pode chamar outro future: acabou o encadeamento. Por isso a orientação da própria plataforma há anos é clara: em código novo, use Queueable. Future sobrevive por compatibilidade e por um punhado de contextos legados onde Queueable ainda não chega. Se você está escrevendo um future hoje sem um motivo específico, está escrevendo dívida técnica.

Queueable: o cavalo de batalha do async moderno

Queueable é o future feito direito. Ele resolve exatamente as três limitações: aceita tipos complexos, devolve um Id de job para monitorar, e encadeia. A interface é uma só, com um método:

public class RecalculoCarteira implements Queueable {
    private List<Account> contas;

    public RecalculoCarteira(List<Account> contas) {
        this.contas = contas;
    }

    public void execute(QueueableContext ctx) {
        // trabalho pesado aqui, com objetos ricos na mao
        for (Account a : contas) {
            // recalcula, monta, decide
        }
        update contas;
    }
}

Você enfileira com System.enqueueJob(new RecalculoCarteira(minhasContas)), e recebe de volta o Id do job. O ganho real, porém, é o encadeamento. A partir do execute, você pode enfileirar o próximo Queueable, e é assim que se processa uma sequência de etapas sem tentar fazer tudo numa transação:

public void execute(QueueableContext ctx) {
    processarLote(this.lote);
    if (temMaisLotes()) {
        System.enqueueJob(new RecalculoCarteira(proximoLote()));
    }
}

Cada elo da cadeia é uma transação nova, com o conjunto de governor limits renovado. É a forma correta de dividir um trabalho grande em pedaços que cabem, sem a maquinaria do Batch. A pegadinha mora no teste: em contexto de teste, apenas um job encadeado executa, para o test run não entrar em loop infinito. Então o teste cobre a lógica do execute, e a cadeia em si você valida com um caso curto. Quando o Queueable também faz callout, ele precisa declarar isso implementando Database.AllowsCallouts, o mesmo cuidado de sempre com chamadas externas seguras.

Regra prática: se o trabalho cabe em uma transação, ou em uma cadeia curta de transações, e você quer dados ricos e monitoramento, é Queueable. Notificação proativa, sincronização de um punhado de registros, orquestração de etapas. Foi Queueable, aliás, que usei como motor no post sobre notificações proativas com Apex: o trabalho de montar e disparar a notificação sai da transação do usuário e roda desacoplado.

Batch Apex: quando o volume não cabe numa transação

O Batch existe para um problema que Queueable não resolve: percorrer mais registros do que cabem nos limites de uma única transação. Ele não processa tudo de uma vez. Ele quebra o conjunto em blocos, e cada bloco roda numa transação própria, com governor limits zerados. A classe tem três métodos, e cada um tem um papel:

public class RecalculoCarteiraBatch implements Database.Batchable<SObject> {

    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id, AnnualRevenue FROM Account WHERE Ativo__c = true'
        );
    }

    public void execute(Database.BatchableContext bc, List<Account> escopo) {
        for (Account a : escopo) {
            // recalcula este bloco
        }
        update escopo;
    }

    public void finish(Database.BatchableContext bc) {
        // roda uma vez, no fim de tudo: notifica, encadeia, fecha
    }
}

O start define o universo, e usa um QueryLocator que suporta até 50 milhões de registros, muito além do teto de uma SOQL comum. O execute recebe um bloco por vez, por padrão de 200 registros, e roda uma vez por bloco. O finish roda uma única vez, quando tudo termina.

A conta que faz o Batch valer a pena é essa: imagine dois milhões de contas para recalcular. Você dispara com Database.executeBatch(new RecalculoCarteiraBatch(), 200). O Salesforce quebra isso em dez mil execuções independentes de 200 registros. Cada uma tem seus próprios 100 SOQL, seus próprios 150 DML, seus próprios 6 MB de heap. Um bloco que falha por um dado ruim não derruba os outros dez mil. Tente fazer isso num future ou num Queueable único e você estoura no primeiro punhado de milhares.

O tamanho do bloco é decisão sua, e importa. Bloco menor renova limites com mais frequência, útil quando cada registro consome muito processamento ou dispara automação pesada; bloco maior reduz o overhead de iniciar transação, útil quando o processamento por registro é leve. Se cada conta que você atualiza aciona trigger, Flow e rollup, um escopo de 200 pode ser demais: baixe para 50. Para guardar estado entre blocos, como um contador ou um acumulador, a classe implementa Database.Stateful, senão cada bloco esquece o anterior.

Regra prática: se o número de registros pode passar do que cabe nos limites de uma transação, é Batch. Sempre. Não existe "por ora são poucos": a carga que mata é a que ninguém previu.

Schedulable: rodar no horário, não no evento

Os dois primeiros respondem a um gatilho: alguém salvou um registro, um código enfileirou o job. O Schedulable responde ao relógio. A interface tem um método, e o que ele faz lá dentro é com você:

public class RecalculoNoturno implements Schedulable {
    public void execute(SchedulableContext ctx) {
        Database.executeBatch(new RecalculoCarteiraBatch(), 200);
    }
}

Você agenda com uma expressão cron:

// toda madrugada as 2h
System.schedule('Recalculo noturno', '0 0 2 * * ?', new RecalculoNoturno());

Repare no que o execute do Schedulable faz: quase nada por conta própria. Ele dispara o Batch. Esse é o ponto que junta tudo.

Combinar os três: o padrão que resolve a maioria dos casos

Na prática, os modelos não competem, eles se encaixam. O padrão de assíncrono mais comum em produção não é escolher um, é agendar um processamento em massa que continua desacoplado no fim. A cadeia inteira fica assim:

  1. Um Schedulable acorda às 2h da manhã.
  2. Ele enfileira um Batch que percorre milhões de registros em blocos.
  3. No finish do Batch, um Queueable é enfileirado para o trabalho leve de fechamento: montar o resumo, disparar a notificação, chamar um sistema externo.

Na org de demonstração que mantenho para a Vetra Distribuidora (empresa fictícia do meu portfólio), é exatamente esse o desenho da recalculada de carteira. Um Schedulable dispara às 2h; o Batch reprocessa as contas ativas em blocos de 200; o finish enfileira um Queueable que faz o callout para o sistema de BI e cria uma notificação proativa para cada gerente cujo território mudou de faixa. Cada peça faz uma coisa, no eixo certo: o Schedulable cuida do tempo, o Batch cuida do volume, o Queueable cuida do encadeamento e do callout. Nenhuma delas tenta fazer o trabalho da outra, e por isso nenhuma estoura.

public void finish(Database.BatchableContext bc) {
    // volume ja foi tratado; agora o fechamento leve e desacoplado
    System.enqueueJob(new NotificarGerentes(bc.getJobId()));
}

Esse encaixe é o que transforma três recursos separados numa arquitetura. Assim como um trigger handler organiza o que roda em cada contexto, a escolha consciente entre os três modelos de assíncrono organiza o que roda em cada eixo: tempo, volume, encadeamento.

Tabela de decisão: qual escolher

Quando a dúvida bater, a pergunta não é "qual é o mais poderoso", é "qual eixo o meu problema tem". A tabela abaixo resume o critério:

Modelo Use quando Limite prático Aceita callout Encadeia
Future Só em código legado ou contexto que Queueable não alcança Parâmetros só primitivos, sem monitoramento Sim, com callout=true Não
Queueable Trabalho curto, dados ricos, etapas encadeadas Cabe em uma transação por elo da cadeia Sim, com Database.AllowsCallouts Sim
Batch Volume acima do que cabe numa transação Até 50 milhões de registros no QueryLocator Sim, com Database.AllowsCallouts Sim, via finish
Schedulable Disparo por horário, não por evento Poucos jobs agendados simultâneos por org Indiretamente, via o que dispara Sim, dispara Batch ou Queueable

A leitura da tabela é direta. Comece pelo eixo: é tempo, é volume, ou é encadeamento e desacoplamento? Se for volume, é Batch, ponto final. Se for tempo, é Schedulable disparando um dos outros. Se for encadeamento com dados ricos e couber nos limites, é Queueable. E Future só entra na conversa se você está mantendo código antigo. Escolher pelo eixo, e não pelo hábito, é o que evita a carga que estoura no dia em que os dados crescem.

Perguntas frequentes

Qual a diferença entre Future e Queueable no Salesforce?

Queueable faz tudo o que Future faz e mais. Aceita tipos complexos como objetos e coleções (não só primitivos), devolve um Id de job para monitorar, e permite encadear um job a partir de outro. Future é o modelo legado. Em código novo, use Queueable; Future só sobrevive por compatibilidade e por contextos legados que Queueable ainda não alcança.

Quantos registros o Batch Apex processa de uma vez?

Ele não processa tudo de uma vez, e é isso que o torna útil. O Batch quebra o conjunto em blocos, por padrão de 200 registros, e cada bloco roda numa transação própria com os governor limits renovados. Dois milhões de registros com escopo 200 viram dez mil execuções independentes, e o QueryLocator do start suporta até 50 milhões.

Como agendar um Batch para rodar toda madrugada?

Você não agenda o Batch direto. Cria uma classe Schedulable cujo execute chama Database.executeBatch da sua classe de Batch, e agenda essa Schedulable com uma expressão cron via System.schedule ou pela tela de jobs agendados. O Schedulable dispara no horário, o Batch faz o trabalho pesado em blocos.

Posso encadear Queueable indefinidamente?

Em produção, na prática não há teto rígido de profundidade de cadeia: cada Queueable enfileira o próximo e o trabalho continua. Em contexto de teste, porém, só um job encadeado executa, para evitar loop infinito no test run. Encadear é a forma correta de continuar o trabalho, em vez de tentar disparar tudo de uma vez.

Segunda opinião técnica

Sua org cresceu. Os riscos também?

Revisão independente de automações, integrações e pontos frágeis, com prioridades claras para decidir o próximo passo.

Entender o diagnóstico →