Warning

Fraudulent domains such as innostaxtech.com or innostaxtechllc.com are NOT affiliated with Innostax. Official communication only comes from @innostax.com. We never request money, banking details, deposits, or equipment purchases during hiring.

Comprehensive Guide to Batch Apex Classes in Salesforce

Master Salesforce Batch Apex with our comprehensive guide. Learn to efficiently process large data sets in Salesforce with expert tips and best practices.

Comprehensive Guide to Batch Apex Classes in Salesforce
Key takeaways
  • 1 Understanding Batch Apex: The nature of batch Apex in Salesforce where it processes huge amount of records in a set of small batches is effective. Prominent sub-operations start, execute, and finish are useful to gather records, process each batch, and perform post-processing, guaranteeing the concise and efficient motion of large data.
  • 2 Governor Limits in Batch Apex: Salesforce places severe governor restrictions for processing time and the number of processed records and heap size. The following is the guide to the limits to adhere to to avoid affecting the utilization of the platform resources, and performance and stability.
  • 3 Benefits and Best Practices: Batch Apex works in batches to process data while also not violating the overall governor limits of an organization’s system. Judicious use of mathematical logic allows the developers to manage the errors and do the cleanup job; thereby creating excellent Salesforce applications that can be scaled up effortlessly.

Introduction to Batch Apex

Processing millions of records inside a single synchronous transaction isn’t possible in Salesforce — governor limits won’t allow it, and even if they did, users would be staring at a spinner for minutes. Batch Apex exists to solve exactly this problem: it lets you break large data operations into smaller chunks, run them asynchronously, and process virtually unlimited records without breaching platform limits. This guide covers how Batch Apex works, its execution lifecycle, governor limits, error handling, scheduling, and when to choose it over alternatives like Queueable Apex.

What Is Batch Apex and When to Use It

Batch Apex is an asynchronous Apex execution mode built for processing large numbers of records — typically anything from a few thousand to tens of millions. Instead of running your logic against an entire dataset in one go, Salesforce divides the records into batches (by default, 200 records each) and processes each batch as a separate transaction, with its own set of governor limits.

This makes Batch Apex the right choice when you need to:

  • Clean up, archive, or update millions of records on a schedule (e.g., nightly data hygiene jobs)
  • Recalculate roll-up fields or perform mass reassignments across an object
  • Process records that exceed synchronous Apex’s row and CPU-time limits
  • Run data migrations or one-time bulk corrections triggered from an admin action

It is not a good fit for real-time, user-facing operations. If a user is waiting for a result after clicking a button, Batch Apex’s asynchronous nature and queuing delays make it the wrong tool — a synchronous DML operation or Queueable Apex job is usually more appropriate there.

Why Salesforce Enforces Governor Limits at All

It is essential to understand why Salesforce enforces governor limits before learning how to work with them because the technical explanations for the limitations quickly become frustrating if you are debugging a governor limit error at 2:00 AM.

Salesforce is a multi-tenant platform, meaning that all clients use the same underlying servers to host their instances of the application. Therefore, the company has to limit the resources that any single transaction can consume in order to ensure that no organization can bring the system to a halt by running a particularly resource-heavy operation. The existence of governor limits is intrinsically linked to the way Salesforce is built and operated, and it is not possible to change these limits for larger transactions, even for companies that operate on Enterprise or Unlimited Editions.

The reason for this design choice, in turn, is closely related to the existence of Batch Apex and the fact that big jobs have to be split into smaller transactions. Essentially, Salesforce has to limit the resources that individual transactions can consume in order to prevent any single client from utilizing the entire capacity of the shared servers. However, by splitting large batches of database operations into smaller transactions, Apex is able to use the increased limits for each transaction to process significantly more data compared to a standard synchronous program while still following the recommended best practices of asynchronous processing.

Common Batch Apex Mistakes that Should be Avoided Essay

There are several common mistakes related to the language and logic of Batch Apex, which have been identified by developers several times already, so it is crucial to avoid these issues when implementing Batch Apex classes.

The first common mistake is that developers tend to forget that the execute method is called once for each batch of records, not for each record itself. It is essential to remember that the Apex class’s execute method should always contain logic that can process a list of records effectively. For example, if an execute method only contains loops that go through the list of records produced by the start method, this logic alone would suffice for simple field updates. However, any query or DML operation performed within the loop will consume governor limits much quicker, which will cause issues when processing large batches of data.

The second common mistake is that developers tend to assume that their batch job will be processed right away after it has been added to the queue. In reality, the queue can only contain 5 active jobs and 100 jobs in total. Hence, if an organization schedules a job during a busy time and the Apex Flex Queue is full, the job may not be processed for hours or even days. As a result, the job may be resubmitted without considering that it might still be in the queue, thus creating an error. That is why it is essential to check the Apex Jobs tab before rescheduling a new job to see whether the previous instance is still running.

The third common mistake is that developers forget to test their batch Apex class with large amounts of data. If a developer tests their implementation with a small amount of data (say, 200 records in a sandbox) and then tries to run the same job on millions of records in the production environment, it may fail due to the complexity of operations performed for each record. This way, a job can fail when it uses too much heap space or CPU time and is halted by Salesforce for exceeding the governor limits. Hence, it is crucial to test batch Apex jobs with large amounts of data to avoid such issues and ensure that the job can be processed successfully at any stage.

Understanding Batch Apex

// Batch Apex Class Example

global class MyBatchClass implements Database.Batchable<sObject> {

    // Start Method: Initialize and collect records
    global Database.QueryLocator start(Database.BatchableContext bc) {
        // Your SOQL query to fetch records
        return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE CreatedDate >= LAST_N_DAYS:7');
    }

    // Execute Method: Process each batch of records
    global void execute(Database.BatchableContext bc, List<Account> scope) {
        // Your logic to process each record in the batch
        for (Account acc : scope) {
            acc.Description = 'Processed by Batch';
        }

        // Update the modified records
        update scope;
    }

    // Finish Method: Perform any post-processing logic
    global void finish(Database.BatchableContext bc) {
        // Your post-processing logic, if needed
    }
}

Syntax Explanation

  • global class MyBatchClass – Defines a global Apex class named MyBatchClass.
  • implements Database.Batchable<sObject> – Specifies that the class implements the Database.Batchable interface for processing Salesforce records.
  • start method – Initializes and collects records to be processed. It returns a Database.QueryLocator to define the scope of records.
  • execute method – Processes each batch of records. Your logic for record processing goes here.
  • finish method – Performs any post-processing logic after all batches are processed.

Usage

To execute this batch class, you would initiate it using the Database.executeBatch method. For instance:

MyBatchClass myBatch = new MyBatchClass();
Database.executeBatch(myBatch, 200); // 200 is the size of each batch

Batch Functions

In Salesforce Batch Apex, the start, execute, and finish methods play distinct roles in orchestrating the processing of records. Let’s delve into the functionalities of each function:

Start Method:

  • Functionality: Initializes and collects the records that will be processed in batches.
  • Usage:
    • Define the scope of records to be processed using a Database.QueryLocator object.
    • Typically involves constructing a SOQL query to identify records based on certain criteria.
  • Example:
global Database.QueryLocator start(Database.BatchableContext bc) {
    return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE CreatedDate >= LAST_N_DAYS:7');
}

Execute Method:

  • Functionality: Processes each batch of records returned by the start method.
  • Usage:
    • Logic within this method is applied to each record in the batch.
    • The primary area for performing data manipulation, calculations, or other operations.
    • Any DML (Data Manipulation Language) operations should be executed in this method.
  • Example:
global void execute(Database.BatchableContext bc, List<Account> scope) {
    for (Account acc : scope) {
        acc.Description = 'Processed by Batch';
    }
    update scope;
}

Finish Method:

  • Functionality: Executes after all batches have been processed by the execute method.
  • Usage:
    • Typically used for any post-processing or cleanup logic.
    • Can be employed for tasks like sending notifications, logging results, or updating status fields.
  • Example:
global void finish(Database.BatchableContext bc) {
    // Perform any post-processing logic here
}

Batch Apex Lifecycle and Execution Flow

Understanding what happens under the hood will help explain why there are governor limits, and what they actually limit. When Database.executeBatch runs:

  1. Salesforce calls start once to build the QueryLocator or Iterable.
  2. The full record set is split into chunks matching your specified batch size (1–2000; default 200).
  3. Each chunk is placed in the Apex Flex Queue, then moved into the batch job queue as system capacity allows.
  4. execute runs for each chunk independently, each with a clean set of governor limits.
  5. Once every chunk finishes, finish executes exactly once.

Only five batch jobs can be queued or actively executing per org at any given time. Additional jobs wait in the Apex Flex Queue (which holds up to 100 jobs in “Holding” status) until a slot opens. This is a frequent source of confusion for teams that schedule many batch jobs back-to-back — jobs don’t fail, they simply wait.

Governor Limits

Salesforce imposes governor limits on Batch Apex to ensure efficient resource utilization and prevent misuse that could adversely impact the platform’s performance.

The governor limits associated with Batch Apex include:
  1. Total Processing Time:
    • Limit: 120 seconds per batch.
    • Description: The cumulative time taken by all batches within a transaction must not exceed this limit.
  2. Heap Size:
    • Limit: 12 MB for synchronous and 6 MB for asynchronous operations.
    • Description: Represents the total memory consumed by the execution of Batch Apex. It includes the size of all variables and data structures.
  3. Number of Records Processed:
    • Limit: 50 million records.
    • Description: The total number of records processed by all batches in a single transaction should not exceed this limit.
  4. Number of Batch Jobs:
    • Limit: 2,000 concurrent or queued batch jobs.
    • Description: The maximum number of Batch Apex jobs that can be in progress or queued for execution at a given time.
  5. Maximum Query Rows:
    • Limit: 50,000 rows.
    • Description: The number of records retrieved in a query should not exceed this limit.
  6. Maximum Query Locator Rows:
    • Limit: 50 million rows.
    • Description: The number of rows returned by a Database.QueryLocator in the start method should not exceed this limit.
  7. Number of DML Statements:
    • Limit: 150 DML statements.
    • Description: The total number of Data Manipulation Language (DML) operations (insert, update, delete) allowed in a transaction.
  8. Number of Database Methods Invoked:
    • Limit: 250,000 database method calls.
    • Description: The total number of methods that use DML statements, such as insert, update, upsert, delete, undelete, or merge.
  9. Callouts:
    • Limit: 100 callouts per batch.
    • Description: The number of HTTP callouts a batch job can make.

It’s crucial for developers to be mindful of these governor limits while designing and executing Batch Apex jobs to ensure compliance and maintain the performance and stability of the Salesforce platform. Additionally, Salesforce regularly updates its limits, so it’s advisable to refer to the latest Salesforce documentation for the most up-to-date information.

Querying and Processing Records Efficiently

Efficiency inside execute matters as much as the overall design. A few practices consistently prevent limit exceptions:Efficiency inside execute matters as much as the overall design. A few practices consistently prevent limit exceptions:

  • Filter in the query, not in Apex. A tight WHERE clause in start avoids wasted iterations and unnecessary heap usage.
  • Avoid SOQL or DML inside loops. Even within a single batch’s execute call, looping DML statements can exhaust the 150-statement limit quickly if the batch size is large.
  • Use selective, indexed fields in your QueryLocator query where possible, especially on large objects, to avoid query timeouts.
  • Keep the batch size realistic. A smaller batch size (e.g., 50–100) reduces the chance of hitting heap or CPU limits when each record triggers complex logic, callouts, or multiple related DML operations; a larger size (up to 2,000) is fine for simple, lightweight field updates.

Sharing Rules and Execution Context

By default, a Batch Apex class runs in system context, meaning it ignores the running user’s field-level security, object permissions, and sharing rules unless told otherwise. This is different from how a Visualforce controller or an LWC-triggered Apex call typically behaves, and it catches developers off guard when a batch job updates or deletes records a user shouldn’t technically have access to.

To enforce the running user’s sharing rules, declare the class with sharing:

global class MyBatchClass implements Database.Batchable<sObject> {
    // Sharing rules are now respected
}

Without this declaration (or with without sharing, which is also the implicit default for classes that don’t specify either), the batch job operates with full visibility into the object’s records regardless of who scheduled it. This matters most for jobs that update sensitive data, process records across multiple business units, or run on a schedule where the “user” context is really an automated process. As a rule, use with sharing unless the job specifically requires system-level access — for example, an org-wide data cleanup task that must touch every record regardless of ownership.

State Management: Database.Stateful

By default, instance variables in a Batch Apex class are reset before each execute call — the job is stateless. If you need to track information across batches (a running counter, an aggregated error list, a total record count for the finish summary), implement Database.Stateful in addition to Database.Batchable:

global class MyStatefulBatch implements Database.Batchable<sObject>, Database.Stateful {
    global Integer recordsProcessed = 0;

    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator('SELECT Id FROM Contact');
    }

    global void execute(Database.BatchableContext bc, List<Contact> scope) {
        recordsProcessed += scope.size();
    }

    global void finish(Database.BatchableContext bc) {
        System.debug('Total processed: ' + recordsProcessed);
    }
}

Use Database.Stateful sparingly — retained variables persist in memory across the entire job, so large collections held statefully can contribute to heap pressure over a long-running batch.

Error Handling and Monitoring

Because each batch is its own transaction, an unhandled exception in one execute call doesn’t necessarily stop the rest of the job — but it does roll back that batch’s DML and mark it as failed. Two practices make batch jobs production-safe:

  • Wrap DML in Database.update(scope, false) (partial success mode) instead of a plain update statement, so one bad record doesn’t fail the entire batch. Inspect the returned Database.SaveResult[] and log failures.
  • Query AsyncApexJob after the job runs, or in finish, to check NumberOfErrors, JobItemsProcessed, and Status:
AsyncApexJob job = [SELECT Status, NumberOfErrors, JobItemsProcessed, TotalJobItems
                     FROM AsyncApexJob WHERE Id = :bc.getJobId()];

For visibility beyond code, the Apex Jobs page (Setup → Apex Jobs) shows status, batches processed, and errors for every running or completed job, and is the first place to check when a scheduled batch appears to have silently failed or stalled in the Flex Queue.

Scheduling Batch Apex

Batch jobs are commonly run on a recurring schedule using the Schedulable interface:

global class NightlyBatchScheduler implements Schedulable {
    global void execute(SchedulableContext sc) {
        Database.executeBatch(new MyBatchClass(), 200);
    }
}

Alternatively, Database.executeBatch can be launched directly at a set interval using System.scheduleBatch(batchInstance, jobName, minutesFromNow, batchSize), which is convenient for one-off delayed execution without a separate Schedulable class.

Choosing a Batch Size: A Practical Reference

The article’s best practices section touches on batch size, but it’s worth laying out the tradeoff more concretely, since “it depends on the complexity of the work” is true but not always actionable on its own.

Batch sizeBest suited forTradeoff
1–50Records triggering callouts, complex validation, or multiple related DML operations per recordMore total transactions, slower overall completion, but far less risk of heap/CPU limit exceptions
100–200General-purpose field updates, moderate logic, typical default use casesBalanced; the default 200 works for most straightforward jobs
500–2,000Simple, lightweight field updates with minimal per-record logicFastest overall completion, but higher risk of hitting limits if per-record complexity was underestimated

The general rule is that the amount of work done for each record dictates the size of the batches you use. A job that does very little work for each record can get away with using very large batches, but a job that does substantive work (callouts, nested queries, or heavy calculations) for each record needs to use smaller batches so as not to exceed the governor limits for each individual transaction.

Monitoring Fields Worth Tracking on AsyncApexJob

The error handling section mentions querying AsyncApexJob, and it’s worth knowing which fields on that object are actually useful for building real monitoring, beyond the three already referenced:

FieldWhat it tells you
StatusCurrent state — Queued, Processing, Completed, Failed, Aborted
NumberOfErrorsCount of batches that failed with an unhandled exception
JobItemsProcessedHow many batches have completed so far
TotalJobItemsTotal batches the job was split into
ExtendedStatusAdditional detail on why a job failed, when available
CreatedDate / CompletedDateUseful for tracking how long a job actually took to run

Building even a simple scheduled report off these fields — flagging any job with NumberOfErrors above zero, or any job still Processing well past its expected runtime — catches problems days before someone might otherwise notice a downstream data issue and trace it back to a silently failing batch job.

Batch Apex vs. Alternatives

Batch Apex isn’t always the right asynchronous tool. Choosing correctly avoids both governor limit issues and unnecessary complexity.

ApproachBest forKey limitation
Batch ApexLarge volumes (thousands to millions of records), scheduled or on-demand bulk processingAsynchronous only; 5 concurrent jobs per org; queuing delays
Queueable ApexComplex logic on smaller record sets, chaining dependent jobs, callouts after DMLNo native batching of large record sets; job chaining depth limits
Future methodsSimple, isolated async tasks (e.g., a single callout)No job chaining, no Database.Stateful, limited monitoring
Bulk APIData loads and migrations from external systemsExternal-facing; not for in-org Apex business logic

As a rule of thumb: if the record volume is unpredictable or large and the work must run on a schedule or in the background, Batch Apex is usually correct. If the task is a single follow-up action after a trigger or DML operation, Queueable Apex is typically simpler and faster.

Best Practices for Production Use

  • Keep execute logic bulkified — never issue SOQL or DML per individual record.
  • Choose a batch size based on the complexity of the work per record, not habit; 200 is a reasonable default but not universal.
  • Use Database.Stateful only when cross-batch state is genuinely required.
  • Always implement partial-success DML handling and log failures for later review.
  • Monitor AsyncApexJob and set up email or Slack alerts in finish for job failures.
  • Avoid scheduling more batch jobs than your org can realistically run concurrently — remember the 5-job execution ceiling.
  • Test with production-scale data volumes in a sandbox before deploying; governor limit issues rarely show up in small test datasets.

Conclusion

In conclusion, the start, execute, and finish methods in Salesforce Batch Apex collectively provide a robust and organized approach to handling large datasets. The start method initializes the process by determining the scope of records, the execute method processes these batches of records, and the finish method allows for post-processing tasks. This triad of methods not only enables efficient processing of large volumes of data but also provides flexibility and customization options for developers.

Through dividing the data into manageable chunks, Batch Apex not only processes a large amount of data but also enhances the performance and responsiveness of the Salesforce platform, adhering to governor limits. Developers can leverage the functionalities of these methods to implement complex logic, handle errors, and perform cleanup tasks, which in turn enhances the overall performance and responsiveness of Salesforce applications.

Get a Fast Estimate on Your Software
Development Project

Chat With Us

Frequently Asked Questions.

Yes, as long as the org hasn't hit its five-concurrent-job ceiling. Salesforce doesn't prevent multiple instances of the same batch class from running simultaneously, though this can sometimes cause record-level contention if both instances are trying to update overlapping records — something worth designing around explicitly rather than assuming won't happen.

Batches that already completed successfully before the abort keep their committed changes — since each batch is its own transaction, aborting the job doesn't roll back work that already finished. Any batch still in progress at the moment of the abort is stopped, and any batches that hadn't started yet simply never run.

Yes — in a test context, calling Test.startTest() and Test.stopTest() around Database.executeBatch forces the batch to run synchronously within the test, making it possible to assert on results immediately rather than needing to poll for async completion.

Platform Events are built for event-driven, near-real-time communication between systems, typically triggered by something happening rather than run on a schedule. Batch Apex is built specifically for large-scale, chunked processing of existing records — the two solve different problems and are frequently used together rather than as substitutes for each other.

Yes, this is a common pattern called batch chaining, often used when one large operation naturally depends on a prior one completing first. It's worth using deliberately rather than by habit, though, since chaining several batch jobs back-to-back can extend total processing time considerably compared to designing the logic to run within a single job where feasible.

They reset with each individual batch's execute call, which is the entire reason Batch Apex can process millions of records without hitting the same limits a single synchronous transaction would face. This is also why per-record complexity within a single batch matters more than total job size — the limits apply per batch, not cumulatively across the whole job.