The flex queue holds batch jobs waiting to run. Five Apex jobs can be active or queued at once, and up to 100 more sit in Holding status until a slot opens. You can reorder Holding jobs, you cannot reorder anything past that point, and when the queue is full executeBatch throws rather than waiting.
How a batch job actually starts
Calling Database.executeBatch() does not run anything. It submits the job, and the job’s first stop is the flex queue with status Holding. When one of the five concurrent slots frees up, the platform moves the job to Queued and then Preparing, and your start() method runs.
So the status sequence is Holding, then Queued, Preparing, Processing, and finally Completed, Failed or Aborted. A job that sits in Holding for twenty minutes has not failed. It is waiting its turn.
The five concurrent slots are shared across all batch classes in the org, yours and any managed package‘s. A nightly integration job from a package occupies a slot exactly as your own job would.
Reordering
Setup has an Apex Flex Queue page listing jobs in Holding, with arrows to move a job up or down. The job at position 1 is next to run. This is genuinely useful during an incident, when a one-off data fix needs to jump ahead of three long-running nightly jobs.
You can do the same from Apex:
// Move a holding job to the front
Boolean moved = FlexQueue.moveJobToFront(jobId);
// Or place it relative to another job
FlexQueue.moveBeforeJob(jobToMoveId, jobInQueueId);
FlexQueue.moveAfterJob(jobToMoveId, jobInQueueId);
These only work on jobs in Holding. Once a job has moved to Queued it is committed, and there is nothing to do but wait or abort it.
When the queue fills
With 100 jobs in Holding, the next Database.executeBatch() call throws a LimitException. It does not block, it does not retry, it does not queue silently. Code that submits jobs in a loop will fail partway through and leave you with an inconsistent set of submitted work.
Defend against it:
Integer holding = [
SELECT COUNT() FROM AsyncApexJob
WHERE JobType = 'BatchApex' AND Status = 'Holding'
];
if (holding < 90) {
Database.executeBatch(new MyBatch(), 200);
} else {
JobBacklog.defer(payload);
}
The alternative, and the better design when you control the submitting code, is to not submit many jobs at once. One batch job over the whole data set beats fifty batch jobs over slices of it, and chaining a job from the previous job’s finish() method keeps exactly one of yours in the queue at a time.
What else consumes the slots
Queueable jobs share the same concurrency pool. So do scheduled jobs when they fire, and future methods are governed separately but contribute to the org’s overall asynchronous capacity. A long queueable chain running alongside three batch jobs will saturate the five slots on its own.
This is why “my batch job did not run last night” and “the queueable chain took nine hours” are frequently the same incident. Look at the whole async picture, not just the job you were watching.
Monitoring
Apex Jobs in Setup shows everything, including the Holding jobs the flex queue page lists. In SOQL:
SELECT Id, ApexClass.Name, Status, JobItemsProcessed, TotalJobItems,
NumberOfErrors, CreatedDate, CompletedDate
FROM AsyncApexJob
WHERE Status IN ('Holding', 'Queued', 'Preparing', 'Processing')
ORDER BY CreatedDate
Worth putting on a dashboard in any org with meaningful batch volume. Two things to watch: the count of Holding jobs, which tells you how congested the queue is, and the time from CreatedDate to CompletedDate on your critical jobs, which tells you whether congestion is affecting them.
Scheduled jobs are different
A scheduled Apex job that calls executeBatch will throw if the queue is full at the moment it fires, and it will not retry. The scheduled job itself reports as completed. That combination produces the worst kind of failure, where the schedule looks healthy and the work never happened. Log the returned job Id from every executeBatch call, and alert when it is missing.
Frequently asked
Can the 5 concurrent limit be raised? No. It is fixed and not negotiable through Support.
Does a Holding job count against the org’s daily async allowance? That allowance counts executions, so a job only draws on it once it actually runs.
Why is my job stuck in Preparing? The start() method is running. A QueryLocator over millions of rows takes real time before the first execute begins.
Do flex queue positions survive a deployment? Yes. Jobs in Holding are unaffected by metadata deployments, though deploying a changed batch class while its job is queued is asking for trouble.
