---
title: "Database.executeBatch cannot be called from a batch"
url: "https://trailassembly.com/executebatch-cannot-be-called-from-batch/"
topic: "Apex and Development"
published: "2026-09-14T03:28:52+00:00"
updated: "2026-10-06T13:54:45+00:00"
error_codes: "AsyncException"
---

# Database.executeBatch cannot be called from a batch

**The short version:** you called `Database.executeBatch` from somewhere the platform forbids it. The message lists the forbidden places explicitly, and `finish()` is not among them. That is the whole fix: move the call to the finish method.

```apex
System.AsyncException: Database.executeBatch cannot be called from
a batch start, batch execute, or future method.
```

## Why the restriction exists

A batch job’s `execute` method runs once per chunk. A job over 100,000 records with the default scope runs it 500 times. If each of those executions could start another batch job, one `executeBatch` in the wrong method would start 500 jobs, each of which could start 500 more.

The org also allows only five batch jobs to be running or queued at once. Everything beyond that sits in the Apex flex queue, which holds up to 100. A recursive batch launch would fill both in seconds and lock out every other job in the org, including ones belonging to other teams and other packages.

So the platform refuses at the point of the call rather than letting you discover the consequences at scale. The same reasoning covers `start`, which runs once but runs before the job is properly under way, and future methods, which have no natural place to put a successor.

## The correct pattern: chain from finish()

`finish()` runs exactly once, after every chunk has completed. One job, one successor, no fan-out. This is the supported way to run jobs in sequence.

```apex
global class StageOneBatch implements Database.Batchable<SObject> {

    global Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT Id FROM Account WHERE Needs_Sync__c = true');
    }

    global void execute(Database.BatchableContext bc, List<Account> scope) {
        // do the work
    }

    global void finish(Database.BatchableContext bc) {
        Database.executeBatch(new StageTwoBatch(), 200);
    }
}
```

The `BatchableContext` gives you the job ID, so `finish` can query `AsyncApexJob` and decide whether the next stage should run at all:

```apex
AsyncApexJob job = [SELECT Status, NumberOfErrors, JobItemsProcessed
                    FROM AsyncApexJob WHERE Id = :bc.getJobId()];
if (job.NumberOfErrors == 0) {
    Database.executeBatch(new StageTwoBatch(), 200);
} else {
    notifyOwner(job);
}
```

Launching stage two unconditionally when stage one failed half its chunks is a good way to turn one incident into two.

## Self-chaining, and the guard it needs

A batch job can start a fresh instance of itself from `finish`. This is how people process work that keeps arriving, or reprocess records that the first pass could not complete. It is also how orgs end up with a job that has been running since March.

Guard it with a condition that will eventually become false, and make that condition about the data rather than about a counter you increment:

```apex
global void finish(Database.BatchableContext bc) {
    Integer left = [SELECT COUNT() FROM Account WHERE Needs_Sync__c = true];
    if (left > 0 && !Test.isRunningTest()) {
        Database.executeBatch(new StageOneBatch(), 200);
    }
}
```

If the job clears the flag it filters on, the count drops and the chain ends. If it does not clear the flag, you have built an infinite loop, and the `Test.isRunningTest` check means you will not find out from a unit test.

## What is allowed to call executeBatch

- **Synchronous Apex.** A trigger, a controller, anonymous execution, an invocable method. Subject to the concurrent job limits.

- **Scheduled Apex.** The standard way to run a nightly job: a schedulable whose `execute` calls `executeBatch`.

- **A batch job’s finish method.** As above.

- **A [Queueable](https://trailassembly.com/apex-choosing-async/).** A Queueable is not on the forbidden list, and starting a batch from one is a common way to bridge between the two models.

Apex Scheduler has its own limit on concurrently scheduled jobs, so an org that schedules dozens of small jobs can hit a ceiling that has nothing to do with batch at all. Keeping the schedule sparse and chaining from `finish` is generally easier to reason about than scheduling each stage a guessed number of minutes apart.

## Frequently asked

**Can finish() start more than one batch job?** It can, but each of them counts against the five concurrent jobs, and a job started from finish goes into the flex queue if the org is already busy. One successor is the pattern that stays predictable.

**Does this apply to Queueable too?** A Queueable can enqueue one further Queueable, which is a separate limit with a separate message. The two chaining models are independent.

**How do I test a chain?** Test each job on its own inside `Test.startTest()` and `Test.stopTest()`. Chained jobs do not run in a test context, which is why the guard clause above exists.

**What if I need to react to a job finishing from outside Apex?** Query `AsyncApexJob` for the status, or attach a Transaction Finalizer to a Queueable if the work fits that model. There is no callback into external systems on job completion beyond what you write yourself.

---

Source: https://trailassembly.com/executebatch-cannot-be-called-from-batch/
Trail Assembly — independent Salesforce write-ups.
