Straight answer: a synchronous transaction may enqueue 50 Queueable jobs, and you asked for a 51st. The message counts for you: Too many queueable jobs added to the queue: 51. If the enqueue happened from inside another asynchronous job, the ceiling is not 50 at all. It is 1.
System.LimitException: Too many queueable jobs added to the queue: 51
Two different limits with one error message
Where the System.enqueueJob call runs decides which limit applies.
- From synchronous Apex (a trigger, a controller, an anonymous block, an API-invoked method) you get 50 per transaction.
- From inside a Queueable, a batch job, a scheduled job or a future method you get one. That single job is the chain link, and a second enqueue in the same execution throws immediately.
Most reports of this error in the wild are the second case being read as the first. Somebody sees “51” in the message, checks that their loop only runs twenty times, and concludes the platform is wrong. The loop is running inside a Queueable, where the second iteration is already one too many.
The trigger loop that causes it
A trigger enqueuing one job per record is the classic shape:
trigger AccountTrigger on Account (after insert) {
for (Account a : Trigger.new) {
System.enqueueJob(new SyncJob(a.Id)); // dies at record 51
}
}
It passes every test, because tests insert two or three records. It fails the first time somebody loads a file, because a trigger processes up to 200 records in one transaction. The fix is the same as every other bulkification fix: enqueue once and pass the collection.
trigger AccountTrigger on Account (after insert) {
System.enqueueJob(new SyncJob(Trigger.newMap.keySet()));
}
A Queueable can carry non-primitive member variables, including SObjects and collections, which is precisely the advantage it has over a future method. Use it.
Chaining, and how deep it goes
The one-job allowance from inside a Queueable exists so a job can chain to a successor. That is how you process more work than one transaction’s limits allow:
public class SyncJob implements Queueable {
private List<Id> remaining;
public SyncJob(List<Id> ids) { this.remaining = ids; }
public void execute(QueueableContext ctx) {
List<Id> slice = takeSlice(remaining, 100);
doWork(slice);
if (!remaining.isEmpty() && !Test.isRunningTest()) {
System.enqueueJob(new SyncJob(remaining));
}
}
}
Two things about that code. The chain has no depth limit in production and enterprise orgs, so a runaway chain runs until something else stops it. Put a guard on the recursion, not just on the data. And the Test.isRunningTest check is there because a test context does not let a Queueable enqueue another Queueable, so an unguarded chain fails your tests rather than passing them.
Developer Edition and trial orgs do cap chain depth, which produces a confusing “maximum stack depth” style failure in an org that is not production. Chains that work for a customer can fail in a demo org for that reason alone.
How this differs from future methods
Both are asynchronous and both are capped at 50 per synchronous transaction. After that they diverge.
- A future method cannot call another future method. There is no chaining at all.
- A future method takes only primitives and collections of primitives. A Queueable takes any serialisable member.
System.enqueueJobreturns an AsyncApexJob ID you can query for status. A future call returns nothing.- A Queueable can attach a Transaction Finalizer, which runs whether the job succeeded or failed. Future methods have no equivalent, which makes retry logic awkward.
For new code there is very little reason to reach for @future. The one remaining case is that a future method can be marked callout=true in a context where you want the simplest possible fire-and-forget, and even then a Queueable implementing Database.AllowsCallouts does the same job with better diagnostics.
When you genuinely need many jobs
If the work is per-record and heavy, the right tool is usually Batch Apex rather than fifty Queueables. The query locator handles the chunking, limits reset per chunk, and you get a job record with a scope size you can tune. Queueables are for a handful of discrete units of work. Batch is for “every row of this object”.
Frequently asked
Does the 50 include jobs my managed package enqueues? Yes. The limit is per transaction across all code in it, so a package trigger firing alongside yours consumes from the same allowance.
Can I see what is queued? Query AsyncApexJob where JobType = 'Queueable'. The Apex Jobs page in Setup shows the same records with status and any error.
Is there a delay option? Yes, an overload of System.enqueueJob accepts a delay in minutes, which is useful for backing off a chain that is polling something external.
Why does my test only run one link of the chain? Because chaining is blocked in tests. Test each link on its own and assert on what it enqueued rather than trying to run the whole chain.
