Skip to main content

Master Salesforce Governor Limits: SOQL, CPU Timeouts, and Apex Best Practices

๐Ÿ’ฌ In plain words:
Governor limits are like the referee in a shared stadium. Every time you run a transaction, Salesforce gives you a strict budget: a set number of database queries, DML statements, CPU time, and memory. This ensures no single tenant can hog the servers and crash the platform. You cannot fight limits; you must design for them by bulkifying your code, caching data, and moving heavy lifting to asynchronous processes.
๐Ÿ“Œ Example: The Scaling Problem
Imagine a simple "Sync All Contacts" button. It worked perfectly for small boutique clients with a few hundred contacts. But when a bank with 80,000 contacts clicked it, the system threw an error: Too many query rows: 50001. Governor limits didn't fail the developer—they acted as an alarm, signaling that this massive workload belongs in Batch Apex, not a real-time button click.
๐ŸŽฌ Real-Life Example: The Loop That Cost 101 Queries

You write a trigger to enrich a Delivery record with its parent Account's region.

The Old/Bad Way:
for (Delivery__c d : Trigger.new) { Account a = [SELECT Region__c FROM Account WHERE Id = :d.Account__c]; }
You are running one SOQL query for every single record.

Why it is bad:
When you bulk upload 101 deliveries, the trigger hits the 100 SOQL query limit and throws a System.LimitException. This exception cannot be caught. The entire save operation dies, and the data rolls back.

The New/Good Way:
  1. Collect all Account IDs into a Set.
  2. Run exactly one query to fetch the Accounts and store them in a Map<Id, Account>.
  3. Loop through the deliveries again and read the regions directly from the Map.
  4. Use the exact same pattern for saving records: add them to a list, then run a single DML update.
The Payoff: 200 records now cost 1 query instead of 200. The workload comfortably fits the budget, and the limits stop being your enemy.
๐Ÿง  Memory Aid: Budget, Not Enemy
Governor limits represent your per-transaction budget. Don't fight them. Design inside them: Bulkify, Async, Cache.

Core Concept: What Are Governor Limits?

Governor limits exist to enforce multi-tenant fairness. Because thousands of organizations share the same underlying servers, Salesforce sets strict boundaries on what a single transaction can consume.

  • Synchronous Limits: 100 SOQL queries, 50,000 rows retrieved, 150 DML statements, 10,000 rows modified, 10 seconds of CPU time, and a 6MB heap size limit.
  • Asynchronous Limits: Several limits are relaxed in async contexts (Batch, Queueable, Future). You get 200 SOQL queries, 60 seconds of CPU time, and a 12MB heap size.
  • The Golden Rule of Transactions: Limits apply per transaction. Every single piece of automation running in that transaction—your Apex code, triggers, Flows, Process Builders, and managed packages—shares the exact same budget.
  • The Fix is Architectural: You can't outsmart limits with clever syntax tricks. You must fix the underlying architecture. Bulkify your code, cache repetitive data, partition massive data volumes, and defer heavy processing to async workers.
Salesforce Governor Limits and Mitigation Strategies
๐Ÿงญ 360 Card: Governor Limits Summary
  • Rule: Limits are a per-transaction budget, not an enemy. Design your architecture to live comfortably inside them.
  • Gain: They are the reason a neighbor’s poorly written, runaway code cannot take your entire Salesforce instance down.
  • Price: Everything in a transaction shares a single budget. Your custom code, admin-built flows, and installed managed packages are all drinking from the same cup.
  • Limits Snapshot:
    Sync: 100 SOQL, 50k rows, 150 DML, 10k rows written, 10s CPU, 6MB heap.
    Async: 200 SOQL, 60s CPU, 12MB heap.
    Callouts: 100 per transaction, up to 120 seconds total.
  • Mirror (Cloud Auto-Scaling): In platforms like AWS or Azure, you simply pay for extra compute capacity. If bad code loops infinitely, you get a massive bill. Salesforce stops it immediately to protect the ecosystem.
  • At Volume: The CPU limit almost always breaks first. The root cause is rarely one slow method; it is usually a massive cascade of nested automations and triggers firing repeatedly.

Core Q&A

Q: What is your systematic approach when a transaction hits a CPU timeout, beyond just "optimizing the code"?

๐ŸŽฏ Say this first: Profile first using debug limits, then attack the root causes in order: eliminate loops-in-loops, prevent trigger recursion, reduce heavy automation stacking, and finally, move irreducible heavy work to an asynchronous process.

A: Follow this systematic ladder:

  • Profile First: Check the debug logs and profiling info. This tells you exactly where the CPU is burning. Is it in your custom Apex, a massive Flow, a trigger recursively fired by a workflow, or a managed package?
  • Eliminate Re-entry: Implement strict recursion guards. Also, move "same-record" field updates into before-save Flows or Triggers to prevent the record from being saved to the database twice.
  • Hoist Work Out of Loops: Never parse JSON, compile regex, or fetch schema describes inside a for loop. Do it once before the loop begins.
  • Fix Nested Loops: Replace loops-within-loops with Map lookups. This turns an O(n²) performance nightmare into an efficient O(n) operation.
  • Defer the Tail: If the transaction is still too heavy, move the non-critical tail-end work into a Queueable job.
  • The Shared Reality: Say it out loud—your "fast" trigger might be dying only because an inefficient Flow ate 8 seconds of the CPU budget beforehand. Acknowledging this reality is a strong differentiator in interviews.

Follow-Ups (Scenario-Based)

Q1: Which limits are NOT reset by Test.startTest() / Test.stopTest()? How do limits interact with @testSetup?

A1: Calling Test.startTest() gives you a fresh, isolated set of governor limits for the specific code being tested. You get one use per test method.

  • Calling Test.stopTest() forces any queued asynchronous code (future methods, queueable, batch) to execute synchronously, allowing you to assert the results immediately.
  • Methods annotated with @testSetup run in their entirely own, separate limit context before the test methods begin.
  • The Misconception: startTest() does not magically "increase" your limits. It merely isolates the code under test from the data setup overhead. Furthermore, some org-wide limits—like the total number of async invocations allowed within a test suite—still apply across the whole test run.

Q2: An integration partner insists on sending 10k-record payloads via an API, which consistently blows row limits in your triggers. What are your negotiation and technical options?

A2: There is a technical solution and a business discussion.

  • Technically: Instruct them to use the Bulk API 2.0, which automatically chunks data into manageable batches before hitting your triggers.
  • Staging Pattern: Alternatively, accept the raw payload into a "lean" custom staging object that has absolutely zero automation attached to it. Then, use Scheduled Batch Apex to process those staging records at a controlled pace.
  • The Negotiation: Don't frame it as "Salesforce can't handle the data." Frame it as a contract: transaction shaping is required for enterprise scale. Offer them an async acknowledgment pattern—you accept the data instantly to free up their connection, and later send a callback or Platform Event when the processing is successfully completed.

Q3 (Compare): Why does Salesforce impose hard governor limits at all, when other cloud platforms (like AWS) just let you scale up?

A: Multi-tenancy. Tens of thousands of different companies share the exact same hardware resources. To keep neighbors safe from one another, every transaction gets a strictly enforced budget. On AWS, a runaway infinite loop just results in a massive credit card bill at the end of the month. Salesforce stops that loop at the door to prevent system degradation. The cure is good architectural design (bulk, async, cache), never a bigger credit card.

Q4: The profiler shows a managed package is eating half of your CPU budget. What are your options?

A: You cannot rewrite a vendor's code, so you must control what triggers it.

  • Configuration: If the package has a settings panel, turn off features, limit the fields it tracks, or reduce the objects it listens to.
  • Evidence-Based Support: Open a ticket with the vendor. Use your debug logs as hard evidence of the CPU burn, rather than just stating an opinion.
  • Execution Order: Re-architect your own code to run before the package triggers, or move your logic to an asynchronous process so both aren't fighting for the same 10-second budget window.
  • Business Reality: Sometimes, the honest answer is that the package is no longer fit for your enterprise data volume. This shifts the conversation from technical tuning to a "buy versus build" architectural review.

Additional Deep Dives

Q5: How does the Limits Apex class help you at runtime?

A: The Limits class is your real-time dashboard for checking how much of your budget you have burned during a transaction.

  • You can use methods like Limits.getQueries() to see how many SOQL queries you've used, and Limits.getLimitQueries() to see your total allowance (usually 100).
  • Why it matters: You can build defensive programming. For example, if (Limits.getLimitQueries() - Limits.getQueries()) < 5, your code can gracefully abort, log a warning, or spin off the remaining work into a Queueable job instead of hard-crashing with an uncatchable exception.

Q6: What is a "Mixed DML" error, and how does it relate to transaction limits?

A: A Mixed DML error occurs when you try to perform DML on setup objects (like User, Profile, or Role) and standard/custom objects (like Account or Delivery__c) within the exact same synchronous transaction.

  • Salesforce enforces this limit because updating a User's security access could instantly change their permission to update the Account record they are saving alongside it.
  • The Fix: Isolate the transactions. Update the Account synchronously, and move the User update into a @future method or a Queueable class. By breaking it into a new async transaction, the Mixed DML limit is bypassed.