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.
Governor limits are one of the first things that surprise developers coming to Salesforce from other platforms. They impose strict ceilings on resources consumed by each Apex transaction — and exceeding them immediately raises an exception that rolls back execution. Understanding these limits means understanding how Salesforce really works.
Salesforce is a multi-tenant platform — thousands of organisations share the same infrastructure. To ensure one client's code doesn't monopolise resources at others' expense, Salesforce enforces per-transaction limits on each resource type. This isn't a bug or artificial limitation — it's the mechanism that keeps the platform stable at scale.
Every time an Apex trigger fires, a Flow calls Apex code, or a batch runs, a resource counter starts. When a limit is reached, Salesforce raises a System.LimitException and rolls back the entire transaction.
💡 Check limits in real time: the Limits class exposes current counters. Limits.getQueries() returns SOQL consumed, Limits.getLimitQueries() the maximum limit. Useful for instrumenting code and detecting overruns before they hit production.
This is the most frequent mistake on Salesforce, and it catches even experienced developers coming from other languages. Putting a SOQL query inside a for loop consumes one query per iteration — and at 101 records, it's over.
// SOQL in loop = LimitException from 101 records for (Account acc : trigger.new) { List<Contact> contacts = [ SELECT Id, Email FROM Contact WHERE AccountId = :acc.Id // ← one query per Account! ]; // processing... }
// A single query for all records in the 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 // ← a single query ]) { if (!contactsByAccount.containsKey(c.AccountId)) { contactsByAccount.put(c.AccountId, new List<Contact>()); } contactsByAccount.get(c.AccountId).add(c); }
The same mistake exists on the DML side. Doing an update or insert inside a loop consumes one DML operation per iteration. At 151 records, the limit is reached.
for (Account acc : accountsToUpdate) { acc.Description = 'Updated'; update acc; // ← one DML per Account! }
List<Account> toUpdate = new List<Account>(); for (Account acc : accountsToUpdate) { acc.Description = 'Updated'; toUpdate.add(acc); } update toUpdate; // ← a single DML for all
The CPU time limit (10 seconds synchronous) only counts pure Apex execution time — not SOQL query wait time or HTTP call wait time. In practice, it's rarely hit on simple code, but becomes critical with:
The main workaround: go asynchronous. A @future, Queueable or Batchable has 60 seconds of CPU instead of 10.
The heap size limit (6 MB synchronous) is often hit when loading large collections into memory — for example retrieving all fields with SELECT * or storing large lists in static variables.
Database.QueryLocator rather than a list for very large volumes| Limit | Synchronous | Asynchronous | Classic mistake |
|---|---|---|---|
| SOQL Queries | 100 | 200 | SOQL in loop |
| SOQL Rows | 50,000 | 50,000 | SELECT without filter |
| DML Statements | 150 | 150 | DML in loop |
| DML Rows | 10,000 | 10,000 | Batch too large |
| CPU Time | 10s | 60s | Nested loops |
| Heap Size | 6 MB | 12 MB | SELECT * on large object |
| HTTP Callouts | 100 | 100 | Callout in trigger |
| Future methods | 50 | — | @future in loop |
Each Apex trigger can receive up to 200 records in the same batch. All code must be written to process a collection, never a single record. This is rule #1 of Apex development.
When processing risks exceeding synchronous limits, move it asynchronous. Salesforce offers several patterns:
@future — for simple non-chainable operations (HTTP callouts from a trigger…)Queueable — for chainable operations with contextBatchable — for high-volume processing (up to 50M records)Schedulable — for scheduled processingIn development, instrumenting code with System.debug(Limits.getQueries()) allows monitoring real consumption and identifying hotspots before they reach production.
🚨 Watch out for cascading triggers: a trigger on object A that updates object B can fire a trigger on B, which updates C… Governor limits apply to the entire transaction, not each trigger individually. An uncontrolled cascade can exceed limits even with perfectly bulkified individual code.
Governor limits aren't an obstacle — they're guardrails that force writing performant, scalable code. A Salesforce developer who masters limits naturally writes bulkified code, thinks in terms of collections rather than individual records, and chooses the right async pattern for the context. This mastery is what distinguishes code that holds in production from code that breaks as soon as volume increases.
Architecture, développement Apex, revue de code — je peux vous accompagner. Réponse sous 24h.Architecture, Apex development, code review — I can support you. Reply within 24 hours.
Parlons de votre projet →Let's talk →