O código roda perfeito no sandbox. Você testa com três registros, tudo verde, sobe para produção. Duas
semanas depois um import de pedidos entra com 250 linhas e o processo morre com Too many SOQL
queries: 101. Ninguém mexeu no código. O que mudou foi o volume. Esse é o dia em que quase todo
dev Salesforce conhece os governor limits, e quase sempre da pior forma.
Governor limits são tetos que o Salesforce impõe a cada transação Apex: quantas queries SOQL, quantos DML, quanto tempo de CPU e memória. Como a org divide servidor com outros clientes, impedem que um código derrube a instância inteira. O erro clássico é query dentro de loop: na 101ª iteração estoura. A solução é bulkificar, buscar tudo fora do loop.
Por que a plataforma limita seu código
O Salesforce é multitenant: a sua org, a do concorrente e mais milhares de outras rodam sobre a mesma infraestrutura física. Não existe um servidor só seu que você pode saturar. Se um Apex mal escrito pudesse abrir dez mil queries numa transação, ele degradaria a performance de todo mundo que dividisse aquele nó. Os governor limits são o mecanismo que impede isso: são um contrato de "até aqui você pode ir".
A consequência prática é dura para quem vem de outras plataformas. Num backend próprio, código
ineficiente fica lento; no Salesforce, código ineficiente simplesmente para com uma
exceção que você não consegue capturar para ignorar. Um LimitException não é um erro comum:
ele encerra a transação e faz rollback de tudo. Não dá para dar try/catch e seguir em
frente. Isso muda a forma de escrever: você projeta para o limite desde a primeira linha, não otimiza
depois que ficou lento.
Quais são os limites que mais estouram
A lista oficial tem dezenas de limites, mas na prática cinco ou seis respondem por quase todos os
incidentes. Estes são os valores por transação, e o ponto que quase ninguém nota de primeira: eles são
por transação, não por registro. Se um único insert de 200 contas dispara
uma trigger, os 200 registros compartilham o mesmo orçamento de 100 queries e 150 DML.
| Limite | Síncrono | Assíncrono |
|---|---|---|
| Queries SOQL | 100 | 200 |
| Registros por query | 50.000 | 50.000 |
| Statements DML | 150 | 150 |
| Registros por DML | 10.000 | 10.000 |
| Tempo de CPU | 10.000 ms | 60.000 ms |
| Heap (memória) | 6 MB | 12 MB |
| Callouts HTTP | 100 | 100 |
Repare em dois detalhes. O limite de queries dobra no assíncrono (Batch, Queueable,
@future), assim como o de CPU sobe de 10 para 60 segundos, e é por isso que processamento
pesado migra para lá. E o limite de tempo de CPU não conta espera de banco ou de
callout, só o processamento do seu código: um loop aninhado sobre listas grandes, ou muita manipulação
de string, é o que costuma estourar os 10 segundos, não a query em si.
Por que o código quebra só em produção
Este é o ponto que pega o dev de surpresa. O limite é por transação e a transação síncrona típica lida com um lote de até 200 registros. No sandbox você testa com dois ou três, então o loop roda duas ou três vezes e nunca chega perto de 100 queries. Em produção, um import, uma integração ou uma atualização em massa manda 200 de uma vez, e o mesmo código executa o loop 200 vezes. O bug sempre esteve lá; o volume só o revelou.
Vou dar um caso concreto da Vetra Distribuidora, a empresa fictícia que uso como org de
demonstração do portfólio. Havia uma trigger em OrderItem que, para cada linha do pedido,
buscava o Product2 relacionado para copiar o nome e a família do produto. Com um pedido de
cinco itens, cinco queries, tudo bem. Quando simulei um import de reposição de estoque com um pedido de
250 linhas, a transação morreu na 101ª linha com Too many SOQL queries: 101. O pedido
inteiro sofreu rollback: nenhuma linha foi gravada, e o usuário viu uma tela de erro sem entender por quê.
O código nunca tinha sido tocado. Só nunca tinha visto volume.
Como bulkificar: o padrão que resolve quase tudo
Bulkificação é a disciplina de escrever código que processa uma coleção de registros com um número constante de queries e DMLs, independente de o lote ter 1 ou 200 registros. A receita tem três passos que você repete em quase toda situação: colete os IDs primeiro, faça uma query só, depois trabalhe em memória com um Map.
Este é o padrão errado, o que estourou na Vetra. Uma query por iteração:
// NÃO faça: uma query dentro do loop
for (OrderItem item : itens) {
Product2 p = [SELECT Name, Family FROM Product2 WHERE Id = :item.Product2Id];
item.Description = p.Name;
// 250 itens = 250 queries = erro na 101ª
E este é o mesmo trabalho bulkificado. Uma query só, resultado num Map indexado por ID, e o loop apenas lê de memória:
// 1. colete os IDs
Set<Id> prodIds = new Set<Id>();
for (OrderItem item : itens) prodIds.add(item.Product2Id);
// 2. uma query para todos
Map<Id, Product2> prods = new Map<Id, Product2>(
[SELECT Id, Name, Family FROM Product2 WHERE Id IN :prodIds]);
// 3. trabalhe em memória, zero query no loop
for (OrderItem item : itens) {
Product2 p = prods.get(item.Product2Id);
item.Description = p.Name;
}
A diferença não é estilo. Com o padrão bulkificado, o pedido de 250 linhas gasta uma
query em vez de 250, e o mesmo código atende com folga um pedido de 5.000 linhas. O construtor
new Map<Id, Product2>(...) aceita direto o resultado da query e indexa por ID, o que
torna o lookup dentro do loop instantâneo. Esse é o feijão com arroz da boa
prática em Apex: nenhuma query ou DML nunca deve morar dentro de um loop.
DML dentro de loop: o outro erro clássico
Query no loop estoura o limite de 100. O irmão gêmeo do problema é o DML no loop, que estoura o de 150. O padrão perigoso atualiza registro por registro:
// NÃO faça: um update por iteração
for (Account a : contas) {
a.Status__c = 'Ativo';
update a; // 200 contas = 200 DMLs = erro no 151º
}
A correção é acumular numa lista e fazer um único DML sobre ela, fora do loop:
List<Account> paraAtualizar = new List<Account>();
for (Account a : contas) {
a.Status__c = 'Ativo';
paraAtualizar.add(a);
}
update paraAtualizar; // um DML para a lista inteira
Um update sobre uma lista de 200 registros conta como um statement DML, não
200. É a mesma lógica da query: a operação é feita em lote. Essa decisão de onde a lógica roda, em
linguagem declarativa ou em código, é parte do que discuto em quando
usar Flow ou Apex: o Flow bulkifica boa parte disso por você, e é uma das razões legítimas para não
escrever Apex quando o Flow resolve.
Como enxergar o limite antes de estourar
Você não precisa adivinhar quão perto está do teto. A classe Limits reporta o consumo atual
e o máximo de cada recurso em tempo de execução. Jogar isso num System.debug nos pontos
críticos mostra exatamente onde o orçamento está sendo gasto:
System.debug('Queries: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
System.debug('DML: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());
System.debug('CPU: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
Mas o debug depois do problema é remédio, não prevenção. A prevenção de verdade é o teste com
volume. Um teste unitário que insere 2 registros nunca vai revelar um bug de bulkificação. O
teste tem que criar um lote realista, de 200 registros, dentro de Test.startTest() e
Test.stopTest(), que reinicia os contadores de limite e garante que a sua lógica roda dentro
de um orçamento limpo:
@isTest
static void deveProcessar200SemEstourar() {
List<Account> contas = new List<Account>();
for (Integer i = 0; i < 200; i++) {
contas.add(new Account(Name = 'Conta ' + i));
}
Test.startTest();
insert contas; // dispara a trigger com 200 de uma vez
Test.stopTest();
System.assertEquals(200, [SELECT COUNT() FROM Account WHERE Name LIKE 'Conta %']);
}
Se esse teste passa, a bulkificação está correta. Se ele estoura, você achou o bug no CI, não numa ligação de cliente às sexta à noite. Vale a mesma disciplina de sempre montar a query com segurança: quando o filtro é dinâmico, combine bulkificação com SOQL dinâmico à prova de injection, para não trocar um problema por outro.
Quando isso não é problema seu
Honestidade primeiro: se você é admin e nunca escreveu uma linha de Apex, provavelmente nunca vai bater de frente com esses limites. O Flow, os workflows padrão e a maioria das automações declarativas já operam de forma bulkificada por baixo dos panos. O risco real aparece quando existe código customizado, quando você chama Apex de dentro do Flow, ou quando várias automações disparam umas às outras na mesma transação e o orçamento compartilhado estoura sem que nenhuma delas, isoladamente, pareça pesada.
E não caia no extremo oposto de otimizar o que não precisa. Se a sua trigger nunca vai ver mais que dez registros por vez, escrever máquina de bulkificação elaborada é esforço desperdiçado. A regra é simples e barata: nunca ponha SOQL ou DML dentro de um loop, e teste com 200. Fazendo só isso, você elimina a esmagadora maioria dos incidentes de governor limits sem precisar de mais nada, nem de mim.
Perguntas frequentes sobre governor limits
O que são governor limits no Salesforce?
São tetos que a plataforma impõe a cada transação Apex: número de queries SOQL, statements DML, tempo de CPU, memória e callouts. Como a org é multitenant e divide servidor com outros clientes, esses limites impedem que um código individual consuma recurso demais e degrade a instância dos vizinhos.
Por que aparece o erro Too many SOQL queries: 101?
Porque a transação executou mais de 100 queries SOQL de uma vez. Quase sempre a causa é uma query dentro de um loop: com 101 iterações, a 101ª estoura. A correção é tirar a query do loop e buscar todos os registros num único SOQL antes de iterar.
Governor limits mudam entre síncrono e assíncrono?
Alguns mudam. Em contexto assíncrono (Batch, Queueable, @future) o limite de queries dobra
para 200 e o de CPU sobe para 60 segundos. Não conte com isso como plano A: bulkifique primeiro e use o
assíncrono para volume que realmente precisa dele.
Preciso me preocupar com isso se só uso Flow?
Menos, mas sim. O Flow tem os mesmos limites por trás e já bulkifica boa parte das operações por você. O risco aparece quando você chama Apex de dentro do Flow ou encadeia automações que disparam umas às outras na mesma transação e somam no orçamento compartilhado.
Seu Apex quebra com volume e você não sabe por quê?
Eu reviso triggers e classes Apex procurando query e DML em loop, refatoro para o padrão bulkificado e deixo o teste de 200 registros no lugar para o problema não voltar. Comece com um diagnóstico gratuito de 45 minutos.
Falar no WhatsApp Ver serviços