---
title: "Too many queueable jobs added to the queue: 51"
url: "https://trailassembly.com/too-many-queueable-jobs/"
topic: "Apex and Development"
published: "2026-09-14T14:23:12+00:00"
updated: "2026-10-06T13:55:23+00:00"
error_codes: "LimitException"
---

# Too many queueable jobs added to the queue: 51

**Straight answer:** a synchronous transaction may enqueue 50 [Queueable](https://trailassembly.com/apex-choosing-async/) 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.

```apex
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:

```apex
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](https://trailassembly.com/flow-bulkification/) fix: enqueue once and pass the collection.

```apex
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:

```apex
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.enqueueJob` returns 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](https://trailassembly.com/apex-batch-that-scales/) 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](https://trailassembly.com/1gp-vs-2gp/) 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.

---

Source: https://trailassembly.com/too-many-queueable-jobs/
Trail Assembly — independent Salesforce write-ups.
