Apex governor limits exist because Salesforce runs on shared infrastructure, and one bad loop in your org can't be allowed to starve every other tenant on the same servers. The platform caps how many SOQL queries you can run, how much heap memory you can consume, how many DML statements you can fire, all per transaction. Hit the ceiling and your transaction dies, full stop, no partial save. Most developers meet governor limits for the first time in production, not in a sandbox, which is exactly the wrong place to learn how they work.
Salesforce is multi-tenant. Your org shares database servers, application servers, and compute with thousands of other customers. Without hard limits, one runaway trigger could lock rows, exhaust CPU, and degrade performance for every other company on that instance. Governor limits are the mechanism that keeps that from happening. They're not arbitrary punishment. They're the price of admission to a platform where you don't manage your own servers.
This matters because it changes how you should think about the limits. You can't negotiate them, you can't buy your way past most of them, and support won't raise a synchronous transaction limit because your code is inefficient. The fix is always architectural. Code that respects governor limits from day one scales. Code that ignores them works fine with ten test records and falls over the day a real data load hits it.
The failure mode is brutal and binary. A transaction that exceeds a limit doesn't degrade gracefully. It throws a LimitException, rolls back everything in that transaction, and the user sees an ugly error screen. There's no retry, no partial commit. That all-or-nothing behavior is why governor limit bugs show up as production incidents instead of slow page loads.
Salesforce publishes dozens of limits, but in practice only a handful cause real pain. Here are the ones worth memorizing, with their per-transaction caps in synchronous Apex.
| Limit | Synchronous Cap | What Breaks It |
|---|---|---|
| SOQL queries | 100 | A query inside a for loop |
| Records retrieved by SOQL | 50,000 | Unfiltered queries on large objects |
| DML statements | 150 | insert/update/delete inside a loop |
| Records processed by DML | 10,000 | Bulk data loads without batching |
| CPU time | 10,000 ms | Heavy formula fields, nested loops, recursive triggers |
| Heap size | 6 MB | Loading huge collections or large strings into memory |
| Callouts | 100 | Looping integration calls per record |
Notice the pattern. Almost every one of these gets exceeded the same way: a query, a DML operation, or a callout sitting inside a loop that iterates over a list of records. One record triggers it fine. Two hundred records in a data import triggers the exception. That's the single most common root cause of governor limit failures in real Salesforce orgs, and it has a name.
Bulkification means writing code that processes a list of records in one pass instead of looping and hitting the database once per record. Instead of querying inside a for loop, you query once outside the loop and build a map. Instead of calling insert or update on each record individually, you collect them into a list and call DML once on the whole list.
This single habit eliminates the SOQL-in-a-loop and DML-in-a-loop problems, which together account for the majority of governor limit exceptions reported in Salesforce orgs. It's not an advanced technique. It's table stakes. Any trigger, any batch class, any Flow with an element inside a loop needs to follow this pattern, because Salesforce triggers fire on batches of up to 200 records at a time, and your code needs to handle all 200 in one shot.
Flow makes this easy to get wrong because the builder lets you drag a Get Records or Update Records element inside a loop without warning you. It will run fine in testing with five records. It will throw a limit exception the day someone imports 500 leads through Data Loader. If you're building automation in Flow, move any database operation outside the loop and collect records in a collection variable first. The same discipline that applies to Apex applies here, just with a different interface.
Some jobs are too big for synchronous limits no matter how well you bulkify. Processing 200,000 records, calling an external API for every record in a large list, running a nightly data cleanup across a whole object. For those, Salesforce gives you four asynchronous contexts, each with its own, usually much larger, limits.
Future methods run on their own thread with higher limits but no return value and no guaranteed order. Queueable Apex replaces future methods for anything that needs chaining or complex parameter types, and it gives you a job ID you can track. Batch Apex is built specifically for large-scale operations, processing records in chunks of up to 2,000 at a time, with each chunk getting its own fresh set of governor limits. Scheduled Apex just runs any of the above on a timer.
Batch Apex is the right tool when you're touching more than 10,000 records or need checkpoint-restart behavior. Queueable is the right tool when you need to chain jobs or pass complex objects between asynchronous steps. Picking the wrong one doesn't just waste effort, it reintroduces the same limit problems you were trying to avoid, because each async context still has its own caps.
Most governor limit incidents are preventable before they ever reach production, and the tool that catches them is sitting in every org already: debug logs and the Limits class. Calling Limits.getLimitQueries() and Limits.getQueries() inside your code during testing tells you exactly how close you are to the ceiling, not after a failure, but while you're still writing the method.
Add this check to every piece of Apex that processes a list, especially anything that fires from a trigger. Log the query count, the DML count, and the CPU time at the end of the method during testing with realistic data volumes, not five test records. Salesforce's own test classes let you insert 200 records and run your trigger against them; use that volume every time, because a test that only checks one record will never catch a loop-based limit bug.
Nightly batch jobs deserve the same scrutiny. Set up scheduled Apex to log its own execution stats, and review them weekly. An org that silently creeps toward its daily async Apex execution limit, currently 250,000 or five times the number of licenses, whichever is greater, will eventually start failing jobs with no warning unless someone is watching the trend line.
Governor limits get blamed as a platform weakness by developers who got burned once and never fixed the underlying habit. That's backwards. A hard, predictable ceiling that forces bulk-safe code is better than a system that lets bad code limp along until it quietly degrades performance for everyone. Oracle and SQL Server don't stop you from writing a query that locks a table for ten minutes. Salesforce does. That's a feature, not friction.
The developers who complain the loudest about governor limits are usually the ones who never learned to bulkify in the first place, and every org has legacy code from exactly that era. If you inherit an org with scattered SOQL-in-loop bugs, don't patch each one as it fires. Audit every trigger and batch class for loop-based database calls in one pass, fix the pattern everywhere at once, and the governor limit incidents mostly stop showing up in your error logs for good.
The transaction throws a LimitException and the entire operation rolls back, including any records that were about to be inserted or updated in that same transaction. There's no partial save and no automatic retry. The user sees an error, and the fix always requires changing the code's structure, not just reducing the data volume.
Placing a SOQL query or DML statement inside a for loop. It works fine with a handful of test records but fails once a trigger or batch job processes a real-world volume of 100 or more records at once. The fix is bulkification: move the query or DML call outside the loop and operate on the full list in one call.
Synchronous Apex transactions are capped at 100 SOQL queries, while asynchronous contexts like batch and queueable Apex allow up to 200. Both limits reset per transaction, which is why bulkifying code to use one query per list instead of one query per record is the standard fix.
Yes. Flows run inside the same transaction as any Apex that triggers or follows them and they consume from the same limit pool. A Get Records or Update Records element placed inside a loop in Flow Builder causes the exact same SOQL-in-a-loop or DML-in-a-loop failures that poorly written Apex does.
Use Batch Apex when you need to process more than 10,000 records or want built-in chunking with fresh limits per batch. Use Queueable Apex when you need to chain asynchronous jobs together or pass complex objects between steps, since it offers more flexibility for smaller, sequential workloads.