Saltar al contenido
3 de noviembre de 2025·Actualizado el ·20 min de lectura

Governor Limits en Salesforce: entenderlos, monitorearlos y evitarlos

Una guía práctica sobre los governor limits de Salesforce — los más críticos, cómo monitorear tu consumo y patrones para mantenerte holgadamente dentro de los límites.

← Volver

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ímiteValorCausa habitual
Consultas SOQL100SOQL dentro de un loop
Sentencias DML150DML dentro de un loop
Filas DML10.000Operaciones masivas
Filas SOQL50.000Recuperar tablas completas
Tamaño del heap6 MBAlmacenar datasets grandes
Tiempo de CPU10.000 msLoops complejos, algoritmos
Callouts100Llamadas a APIs externas

Límites asíncronos por transacción (Batch, Future, Queueable)

LímiteValor
Consultas SOQL200
Sentencias DML150
Filas DML10.000
Tamaño del heap12 MB (todo el Apex asíncrono)
Tiempo de CPU60.000 ms

Límites diarios a nivel de org

LímiteValor
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 APIVarí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 total

3. 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

  1. Revisa los debug logs — identifica qué línea lanzó la excepción
  2. Encuentra la causa raíz — normalmente un trigger que llama a un método que hace SOQL/DML dentro de un loop
  3. 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)
  4. Refactoriza — aplica patrones de bulkificación
  5. 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 toProcess

Seguimiento 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ónProblemaSolución
SOQL en loopLímite de 100 consultasRecolectar IDs, consultar una vez con IN
DML en loopLímite de 150 DMLRecolectar registros, DML una sola vez
SOQL grande sin LIMITLímite de 50.000 filasAñadir WHERE, LIMIT, o usar agregación
Recursión de triggerLoop infinitoGuarda de recursión basada en los Id procesados
Callout desde un triggerLos 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 SObjectHeap de 6MBUsar 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:

Aïcha Imène DAHOUMANE
Sobre la autora
Aïcha Imène DAHOUMANE
Administradora y Desarrolladora Salesforce

Desarrolladora Salesforce con formación en infraestructura IT — construyo soluciones fiables y mantenibles con Apex, Flows, LWC y CI/CD.

CompartirCompartir en LinkedIn