Los governor limits son las reglas que Salesforce impone para garantizar que ningún org monopolice los recursos compartidos de la plataforma. Entenderlos es esencial — alcanzarlos en producción provoca excepciones, automatizaciones rotas y usuarios descontentos.
Por qué existen los governor limits
Salesforce es una plataforma multi-tenant. Tu org comparte cómputo, base de datos y recursos de API con miles de otros. Los governor limits evitan que un solo org consuma recursos de forma desproporcionada. No son errores — son la arquitectura misma.
Los límites que realmente muerden
Límites síncronos por transacción
| Límite | Valor | Causa habitual |
|---|---|---|
| Consultas SOQL | 100 | SOQL dentro de un loop |
| Sentencias DML | 150 | DML dentro de un loop |
| Filas DML | 10.000 | Operaciones masivas |
| Filas SOQL | 50.000 | Recuperar tablas completas |
| Tamaño del heap | 6 MB | Almacenar datasets grandes |
| Tiempo de CPU | 10.000 ms | Loops complejos, algoritmos |
| Callouts | 100 | Llamadas a APIs externas |
Límites asíncronos por transacción (Batch, Future, Queueable)
| Límite | Valor |
|---|---|
| Consultas SOQL | 200 |
| Sentencias DML | 150 |
| Filas DML | 10.000 |
| Tamaño del heap | 12 MB (todo el Apex asíncrono) |
| Tiempo de CPU | 60.000 ms |
Límites diarios a nivel de org
| Límite | Valor |
|---|---|
| Ejecuciones de Apex asíncronas (Batch, Future, Queueable, Scheduled) | 250.000 o 200 × el número de licencias de usuario, el valor más alto, en 24 h móviles |
| Alertas de email (reglas de flujo de trabajo, aprobaciones, flows) | 1.000 por licencia estándar de Salesforce (15 en Developer Edition), con un máximo de 2.000.000 por org |
| Llamadas a la API | Varía según la edición (Developer: 15.000/día) |
Estos límites de org se consultan en cualquier momento con OrgLimits.getMap() en Apex o el recurso REST /limits.
Monitorear tus límites
En código Apex
// Comprueba los límites restantes en cualquier punto de la ejecución
System.debug('SOQL queries: ' + Limits.getQueries() + '/' + Limits.getLimitQueries());
System.debug('DML statements: ' + Limits.getDmlStatements() + '/' + Limits.getLimitDmlStatements());
System.debug('DML rows: ' + Limits.getDmlRows() + '/' + Limits.getLimitDmlRows());
System.debug('CPU time: ' + Limits.getCpuTime() + '/' + Limits.getLimitCpuTime());
System.debug('Heap: ' + Limits.getHeapSize() + '/' + Limits.getLimitHeapSize());Guardia proactiva antes de operaciones críticas
public class LimitGuard {
public static Boolean hasSoqlCapacity(Integer needed) {
return (Limits.getLimitQueries() - Limits.getQueries()) >= needed;
}
public static Boolean hasDmlCapacity(Integer needed) {
return (Limits.getLimitDmlStatements() - Limits.getDmlStatements()) >= needed;
}
public static Boolean isHeapSafe(Integer safetyMarginBytes) {
return (Limits.getLimitHeapSize() - Limits.getHeapSize()) > safetyMarginBytes;
}
}
// System.LimitException no se puede instanciar ni capturar:
// se lanza una excepción personalizada que el llamador puede gestionar.
public class InsufficientCapacityException extends Exception {}
// Uso
if (!LimitGuard.hasSoqlCapacity(5)) {
throw new InsufficientCapacityException('Capacidad SOQL insuficiente para continuar.');
}Los patrones que evitan la mayoría de las violaciones
1. Nunca hagas SOQL o DML dentro de un loop
Esta es la causa número uno de violaciones de governor limits. Una consulta por registro = 201 consultas para 201 registros.
// ❌ SOQL dentro de un loop — alcanza el límite en el registro 101
for (Opportunity opp : opportunities) {
Account acc = [SELECT Name FROM Account WHERE Id = :opp.AccountId];
// ... usar acc
}
// ✅ Patrón seguro para bulk
Set<Id> accountIds = new Set<Id>();
for (Opportunity opp : opportunities) accountIds.add(opp.AccountId);
Map<Id, Account> accountMap = new Map<Id, Account>(
[SELECT Id, Name FROM Account WHERE Id IN :accountIds]
);
for (Opportunity opp : opportunities) {
Account acc = accountMap.get(opp.AccountId);
// ... usar acc
}2. Recolecta y ejecuta el DML una sola vez
// ❌ DML dentro de un loop
for (Contact c : contacts) {
c.Status__c = 'Processed';
update c; // 1 DML por registro
}
// ✅ Recolectar y luego hacer DML una sola vez
List<Contact> toUpdate = new List<Contact>();
for (Contact c : contacts) {
c.Status__c = 'Processed';
toUpdate.add(c);
}
if (!toUpdate.isEmpty()) update toUpdate; // 1 DML en total3. Usa @future o Queueable para los callouts
Un trigger no puede hacer un callout síncrono, y un callout después de un DML no confirmado en la misma transacción falla («You have uncommitted work pending»). Descárgalos:
public class ExternalSyncService {
@future(callout=true)
public static void syncToERP(List<Id> accountIds) {
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :accountIds];
// ... hacer llamadas HTTP
}
}
// Invocación desde el trigger
trigger AccountTrigger on Account (after insert) {
ExternalSyncService.syncToERP(
new List<Id>(new Map<Id, Account>(Trigger.new).keySet())
);
}4. Usa SOQL de agregación para reducir el conteo de filas
// ❌ Recuperar 50k filas solo para contarlas
List<Opportunity> all = [SELECT Id FROM Opportunity WHERE AccountId = :accId];
Integer count = all.size(); // ¡usa 50k de tu límite de 50k filas!
// ✅ Agregar a nivel de base de datos
Integer count = [SELECT COUNT() FROM Opportunity WHERE AccountId = :accId];5. Gestiona el heap liberando referencias con null
public void execute(Database.BatchableContext bc, List<Account> scope) {
// Procesa la primera mitad
processPhaseOne(scope);
// Pone explícitamente a null los datos intermedios grandes para liberar heap antes de la fase 2
// (el recolector de basura de Salesforce lo reclamará en la próxima asignación)
List<Account> processed = null;
// Procesa la segunda fase con más heap disponible
processPhaseTwo(scope);
}Queueable Apex: mejor que @future
Los jobs Queueable son el reemplazo moderno de @future — soportan encadenamiento y parámetros con tipado seguro:
public class SyncQueueable implements Queueable, Database.AllowsCallouts {
private List<Id> recordIds;
public SyncQueueable(List<Id> ids) {
this.recordIds = ids;
}
public void execute(QueueableContext ctx) {
// Hacer el trabajo — puede hacer callouts (Database.AllowsCallouts)
List<Account> accounts = [SELECT Id, Name FROM Account WHERE Id IN :recordIds];
// ... procesar
// Encadena el siguiente job si queda más trabajo
if (moreWorkNeeded()) {
System.enqueueJob(new SyncQueueable(nextBatch));
}
}
}
// Encolar
System.enqueueJob(new SyncQueueable(accountIds));Límite: Máximo 50 jobs Queueable encadenados por transacción. Máximo 2 jobs Queueable encolados desde una sola transacción síncrona.
Qué hacer cuando alcanzas un límite
LimitException en producción
- Revisa los debug logs — identifica qué línea lanzó la excepción
- Encuentra la causa raíz — normalmente un trigger que llama a un método que hace SOQL/DML dentro de un loop
- Comprueba la recursión — ¿el trigger se está llamando a sí mismo? Usa una guarda de recursión basada en los Id ya procesados (un simple booleano ignora los lotes siguientes de 200 registros)
- Refactoriza — aplica patrones de bulkificación
- Considera lo asíncrono — mueve el procesamiento pesado a Batch o Queueable
Escenario habitual: doble disparo de trigger + Flow desencadenado por registro
// Un trigger se dispara → un Flow desencadenado por registro actualiza registros → el trigger se dispara de nuevo
// Solución: una guarda de recursión basada en los Id ya procesados — un simple booleano
// ignoraría los lotes siguientes de 200 registros (ver el artículo sobre triggers)
public class TriggerControl {
// Clave = trigger + contexto + Id: un registro se procesa una vez por contexto
private static Set<String> processed = new Set<String>();
public static Boolean markProcessed(String triggerName, Id recordId) {
return processed.add(triggerName + ':' + Trigger.operationType + ':' + recordId);
}
}
// En el handler del trigger
List<Account> toProcess = new List<Account>();
for (Account acc : (List<Account>) Trigger.new) {
if (TriggerControl.markProcessed('AccountTrigger', acc.Id)) {
toProcess.add(acc);
}
}
if (toProcess.isEmpty()) return;
// ... tu lógica, solo sobre toProcessSeguimiento de límites en Developer Console
En Developer Console → Logs → abre un debug log → haz clic en la pestaña Execution Overview.
Verás un gráfico del consumo de governor limits a lo largo de la transacción — útil para identificar qué método consume más consultas SOQL o heap.
Tabla resumen: antipatrones → soluciones
| Antipatrón | Problema | Solución |
|---|---|---|
| SOQL en loop | Límite de 100 consultas | Recolectar IDs, consultar una vez con IN |
| DML en loop | Límite de 150 DML | Recolectar registros, DML una sola vez |
| SOQL grande sin LIMIT | Límite de 50.000 filas | Añadir WHERE, LIMIT, o usar agregación |
| Recursión de trigger | Loop infinito | Guarda de recursión basada en los Id procesados |
| Callout desde un trigger | Los callouts síncronos no están permitidos en triggers y fallan tras un DML no confirmado («uncommitted work pending») | @future(callout=true) o Queueable + AllowsCallouts |
| Almacenar listas completas de SObject | Heap de 6MB | Usar sets/maps de IDs en su lugar |
Conclusión
Los governor limits te obligan a escribir mejor código — bulkificado, eficiente y consciente del consumo de recursos. Los patrones aquí (consultas masivas, un solo DML, callouts asíncronos, SOQL de agregación) no son workarounds — son la base del desarrollo profesional en Salesforce. Aplícalos con consistencia y rara vez verás una LimitException fuera de los tests.
Recursos útiles: