Les governor limits sont l'une des premières choses qui surprennent les développeurs qui arrivent sur Salesforce depuis d'autres plateformes. Elles imposent des plafonds stricts sur les ressources consommées par chaque transaction Apex, et les dépasser génère immédiatement une exception qui bloque l'exécution. Comprendre ces limites, c'est comprendre comment Salesforce fonctionne vraiment.
Salesforce est une plateforme multi-tenant, des milliers d'organisations partagent la même infrastructure. Pour garantir que le code d'un client ne monopolise pas les ressources au détriment des autres, Salesforce impose des limites par transaction sur chaque type de ressource. Ce n'est pas un bug ni une limitation artificielle, c'est le mécanisme qui rend la plateforme stable à grande échelle.
Chaque fois qu'un trigger Apex se déclenche, qu'un Flow appelle du code Apex ou qu'un batch s'exécute, un compteur de ressources démarre. Quand une limite est atteinte, Salesforce lève une System.LimitException et rollback la transaction entière.
💡 Vérifier les limites en temps réel : la classe Limits expose les compteurs courants. Limits.getQueries() retourne le nombre de SOQL consommés, Limits.getLimitQueries() la limite maximale. Utile pour instrumenter du code et détecter des dépassements avant qu'ils arrivent en production.
C'est l'erreur la plus fréquente sur Salesforce, et elle touche même des développeurs expérimentés qui arrivent d'autres langages. Mettre une requête SOQL à l'intérieur d'une boucle for consomme une requête par itération, et sur 101 enregistrements, c'est terminé.
// SOQL dans la boucle = LimitException dès 101 enregistrements for (Account acc : trigger.new) { List<Contact> contacts = [ SELECT Id, Email FROM Contact WHERE AccountId = :acc.Id // ← une requête par Account ! ]; // traitement... }
// Une seule requête pour tous les enregistrements du batch Set<Id> accountIds = new Set<Id>(); for (Account acc : trigger.new) { accountIds.add(acc.Id); } Map<Id, List<Contact>> contactsByAccount = new Map<Id, List<Contact>>(); for (Contact c : [ SELECT Id, Email, AccountId FROM Contact WHERE AccountId IN :accountIds // ← une seule requête ]) { if (!contactsByAccount.containsKey(c.AccountId)) { contactsByAccount.put(c.AccountId, new List<Contact>()); } contactsByAccount.get(c.AccountId).add(c); }
La même erreur existe côté DML. Faire un update ou un insert à l'intérieur d'une boucle consomme une opération DML par itération. Sur 151 enregistrements, la limite est atteinte.
for (Account acc : accountsToUpdate) { acc.Description = 'Mis à jour'; update acc; // ← un DML par Account ! }
List<Account> toUpdate = new List<Account>(); for (Account acc : accountsToUpdate) { acc.Description = 'Mis à jour'; toUpdate.add(acc); } update toUpdate; // ← un seul DML pour tous
La limite CPU time (10 secondes en synchrone) ne prend en compte que le temps d'exécution Apex pur, pas le temps d'attente des requêtes SOQL ou des appels HTTP. En pratique, elle est rarement atteinte sur du code simple, mais elle devient critique sur :
Le contournement principal : passer en asynchrone. Un @future, un Queueable ou un Batchable dispose de 60 secondes de CPU au lieu de 10.
La limite heap size (6 MB en synchrone) est souvent atteinte quand on charge de grandes collections en mémoire, par exemple en récupérant tous les champs d'un objet avec SELECT * ou en stockant des listes volumineuses dans des variables statiques.
Database.QueryLocator plutôt qu'une liste pour les très grands volumes| Limite | Synchrone | Asynchrone | Erreur classique |
|---|---|---|---|
| SOQL Queries | 100 | 200 | SOQL dans boucle |
| SOQL Rows | 50 000 | 50 000 | SELECT sans filtre |
| DML Statements | 150 | 150 | DML dans boucle |
| DML Rows | 10 000 | 10 000 | Batch trop large |
| CPU Time | 10s | 60s | Boucles imbriquées |
| Heap Size | 6 MB | 12 MB | SELECT * sur gros objet |
| Callouts HTTP | 100 | 100 | Callout dans trigger |
| Future methods | 50 | — | @future en boucle |
Chaque trigger Apex peut recevoir jusqu'à 200 enregistrements dans un même batch. Tout le code doit être écrit pour traiter une collection, jamais un seul enregistrement. C'est la règle n°1 du développement Apex.
Quand un traitement risque de dépasser les limites synchrones, il faut le déplacer en asynchrone. Salesforce propose plusieurs patterns :
@future : pour les opérations simples non chainables (callouts HTTP depuis un trigger…)Queueable : pour les opérations chainables avec contexteBatchable : pour les traitements sur de grands volumes (jusqu'à 50M d'enregistrements)Schedulable : pour les traitements planifiésEn développement, instrumenter son code avec System.debug(Limits.getQueries()) permet de surveiller la consommation réelle et d'identifier les points chauds avant qu'ils arrivent en production.
🚨 Attention aux triggers en cascade : un trigger sur l'objet A qui met à jour l'objet B peut déclencher un trigger sur B, qui met à jour C… Les governor limits s'appliquent à la transaction entière, pas à chaque trigger individuellement. Une cascade mal maîtrisée peut dépasser les limites même avec du code individuel parfaitement bulkifié.
Les governor limits ne sont pas un obstacle, ce sont des garde-fous qui forcent à écrire du code performant et scalable. Un développeur Salesforce qui maîtrise les limits écrit naturellement du code bulkifié, pense en termes de collections plutôt que d'enregistrements unitaires, et choisit le bon pattern asynchrone selon le contexte. C'est cette maîtrise qui fait la différence entre un code qui tient en production et un code qui plante dès que le volume augmente.
Architecture, développement Apex, revue de code, je peux vous accompagner. Réponse sous 24h.
Parlons de votre projet →