Salesforce governor limits are hard caps on how much a single transaction can do: how many database queries it can run, how many records it can update, how much memory it can use. They exist because Salesforce runs on shared, multi-tenant servers, and one badly written trigger could otherwise slow down every other customer on the same box. Hit a limit and your transaction dies immediately with an uncatchable exception. No partial save, no warning. It just stops.
Most developers learn about governor limits the hard way: a trigger that worked fine in testing suddenly throws a "Too many SOQL queries: 101" error in production when someone imports 500 records at once. That single error has probably cost the Salesforce ecosystem more debugging hours than any other line of code. It is not a bug in Salesforce. It is the platform doing exactly what it was built to do.
Salesforce is not a server you rent. It is a shared environment where thousands of orgs run on the same infrastructure. Without hard caps, one org's inefficient code could consume enough CPU and database resources to degrade performance for every other tenant. Governor limits are the mechanism that keeps one customer's mistake from becoming everyone's outage.
This is different from how most developers think about performance. In a dedicated server environment, a slow query just means a slow page load. Someone notices, someone optimizes, life goes on. In Salesforce, a slow or inefficient pattern does not just run slow. It gets killed outright once it crosses a threshold. The platform trades graceful degradation for strict enforcement.
That tradeoff is the right one. Multi-tenant architecture only works if bad actors and bad code get stopped before they spread. The cost is that Salesforce developers have to write differently than developers on other platforms, and that difference is the single biggest thing that separates someone who can write Apex from someone who can write Apex that survives contact with real data volumes.
There are over forty governor limits in Salesforce, but in practice, five of them account for almost every production failure. Here is what they look like per transaction, in synchronous Apex context.
| Limit | Synchronous Limit | What Happens If You Hit It |
|---|---|---|
| SOQL queries | 100 | Uncatchable LimitException, transaction aborts |
| DML statements | 150 | Uncatchable LimitException, transaction aborts |
| Records retrieved by SOQL | 50,000 | Query fails or truncates depending on context |
| Heap size | 6 MB | Out-of-memory exception |
| CPU time | 10,000 ms | Transaction aborts |
Notice the pattern in that table: none of these numbers are small in isolation. A hundred queries sounds like a lot, until you realize that a trigger firing a SOQL query inside a for loop can burn through that limit with a single batch of 200 records. That is the actual failure mode almost every time. It is never one query being too expensive. It is one query, run 200 times, because it sat inside a loop.
Bulkification means writing code that handles a list of records in one pass instead of one record at a time. Salesforce batches operations by default. When you import 200 records, your trigger does not fire 200 times with one record each. It fires once, with a list of 200 records inside Trigger.new. Code that ignores this and queries or updates inside a loop is not slightly inefficient. It is broken, and it will fail the moment someone does a real bulk operation.
Here is the mental shift that fixes almost every governor limit problem: pull your data once, outside any loop, then process it in memory. Query all the related records you need in a single SOQL call using a WHERE clause with the whole set of IDs. Build your logic using maps and sets. Then commit your changes in a single DML statement at the end, not one DML call per record.
This pattern is not advanced Apex. It is baseline competence. Any code review that approves a SOQL or DML statement inside a for loop is a code review that failed at its one job. Salesforce even documents this explicitly, and yet it remains the single most common defect found in production orgs, according to every AppExchange security review and code audit that gets published.
Batch Apex, Queueable Apex, and Future methods each get their own separate governor limit context. A batch job processing 200 records per batch gets a fresh set of 100 SOQL queries and 150 DML statements for every single batch execution. This is why large data volume jobs get pushed into Batch Apex instead of running synchronously: it is not that async code has higher limits, it is that the limits reset with every chunk.
That said, async Apex introduces its own ceiling: you can only have five queueable jobs chained or enqueued at once in some contexts, and batch jobs have daily execution caps depending on your org's edition. Moving work to async does not mean the limits disappeared. It means you traded one constraint for a different, usually more manageable one.
The mistake teams make here is treating async Apex as an escape hatch for lazy code. Chaining ten queueable jobs because your synchronous trigger keeps hitting CPU time limits is not a fix. It is a workaround that adds latency, complexity, and new failure points. Fix the underlying loop first. Reach for async only when the actual data volume genuinely requires it, like a nightly job touching a million records.
Salesforce requires 75% code coverage to deploy, but code coverage tells you nothing about whether your code survives bulk operations. A test that inserts one record and asserts one field value will pass at 100% coverage while still containing a SOQL-in-a-loop bug that takes down a data migration six months later.
Every test class should include at least one bulk test: insert 200 records (the standard Salesforce batch size), run your trigger or class against all 200 at once, and confirm it does not throw a limit exception. This single habit catches the majority of governor limit bugs before they ever reach a sandbox, let alone production.
You can also call Limits.getQueries() and Limits.getDmlStatements() directly inside your code during testing to log exactly how many resources a given operation consumed. This turns governor limits from a mystery you discover in an error log into a number you can watch climb during development, which is a much better place to catch it.
Some governor limit problems are not code problems, they are design problems. Automation sprawl is the biggest offender: an object with five triggers, three flows, and two process builders all firing on the same update event will each consume their own share of the shared limits, because they all run inside the same transaction. Consolidating automation into a single trigger handler per object is not a style preference. It is a direct defense against limit exhaustion.
The other common design mistake is triggering automation off automation. A flow updates a field, which fires a trigger, which updates another object, which fires another flow. Each hop adds to the same transaction's limit consumption, and recursive automation can multiply that consumption fast enough to blow through CPU time limits even on small record counts. Mapping out what fires on what object, in what order, is unglamorous work. It is also the work that actually prevents the 3 a.m. page when a batch job fails.
A governor limit is a hard cap Salesforce enforces on resources like SOQL queries, DML statements, and CPU time within a single transaction. It exists because Salesforce runs on shared, multi-tenant infrastructure and needs to stop one org's code from degrading performance for others. Exceeding a limit throws an uncatchable exception that aborts the entire transaction with no partial save.
In a synchronous Apex transaction, you can run up to 100 SOQL queries before hitting the limit and throwing an exception. Asynchronous contexts like Batch Apex get the same 100-query limit, but that limit resets fresh for each batch execution. The most common way to hit this limit is placing a query inside a for loop instead of querying once outside it.
Bulkification means writing Apex code that processes a list of records in one pass rather than looping through them one at a time with queries or DML calls inside the loop. Salesforce triggers fire once per transaction with a full list of records in Trigger.new, so code should query all needed data once, process it using maps, and commit changes in a single DML statement. Code that is not bulkified will work fine with a single test record and fail the moment someone updates or imports data in bulk.
Yes. Each batch execution in Batch Apex gets its own fresh set of governor limits, including the 100 SOQL query limit and 150 DML statement limit. This is why large data volume operations get moved into Batch Apex instead of running synchronously, since a single synchronous transaction only gets one shot at those limits regardless of how many records it touches.
Write test classes that insert or update at least 200 records at once, matching Salesforce's standard batch size, and run your trigger or class logic against the full set. You can also call methods like Limits.getQueries() and Limits.getDmlStatements() inside your code to log actual resource consumption during test execution. Standard unit tests that only touch one record at a time will pass code coverage requirements while completely missing bulk-related governor limit bugs.