Salesforce

Queueable Apex vs Batch Apex vs Future Methods

Use a Future method only for a simple callout with no complex parameters. Use Queueable Apex for almost everything else, including chaining jobs and passing objects. Use Batch Apex only when you need to process more records than a single transaction can hold, or when you need checkpoint-restart behavior on a long job. Most developers pick the wrong one because they learned Future first and never unlearned it.

The Three Async Tools and What They Actually Do

Salesforce gives you three ways to run code outside the current transaction: Future methods, Queueable Apex, and Batch Apex. All three exist so you can defer work, dodge governor limits, or process more data than a synchronous transaction allows. They are not interchangeable, and the platform does not warn you when you pick the wrong one. Your code just fails later, usually in production, usually with a limit you did not expect.

Future methods showed up first. They are the oldest, the crudest, and the most limited. Queueable Apex arrived to fix Future's biggest problems: no job tracking, no complex parameters, no chaining. Batch Apex is the heavyweight, built for one job only, moving through massive record sets in manageable chunks.

Here is the blunt version: if you are still writing new Future methods in 2024, you are writing legacy code on day one.

Future Methods: The Limits That Make Them Obsolete

A Future method is a static method annotated with @future. It runs asynchronously, in its own transaction, whenever Salesforce gets around to it. That sounds fine until you hit the restrictions.

Future methods only accept primitive parameters. No sObjects, no custom objects, no collections of objects. You can pass a List of Ids. You cannot pass a List of Account records. If you need the full record inside the method, you query it again by Id, which means an extra SOQL query you did not need with a real object reference.

There is no way to monitor a Future method's job status. It does not show up as an AsyncApexJob you can query cleanly for progress. You cannot chain one Future method to call another Future method either; the platform blocks that outright to prevent runaway recursion.

Future methods still have exactly one legitimate use case: a quick, isolated callout from inside a trigger context, where the trigger itself cannot make an HTTP callout because it is already in a DML context. That is a narrow, specific job. Everything else you were doing with Future, Queueable does better.

Queueable Apex: Why It Replaced Future Methods

Queueable Apex fixes every real complaint about Future methods. You implement the Queueable interface instead of tagging a static method, and that single change unlocks three things Future never had.

First, you can pass complex types, including sObjects, custom classes, and collections. The job runs as a class instance with real state, not a stateless function stripped down to primitives.

Second, you get a job Id back from System.enqueueJob(). That Id lets you query AsyncApexJob and check status, track completion, or build retry logic around failures.

Third, Queueable jobs can chain. One Queueable job can enqueue another Queueable job from inside its own execute method. This is how you break a large workload into sequential steps without needing full Batch Apex machinery. The chain depth is capped at 5 in most contexts (unlimited if the org has certain licenses, but do not build architecture assuming that), so treat chaining as a tool for a handful of sequential steps, not an infinite pipeline.

Queueable also respects the same 50 queued jobs per rolling 24-hour period limit that governs Future methods combined with queueable enqueue calls, so this is not a loophole around Salesforce's async ceiling. It is a better shape for the same ceiling.

Batch Apex: When You Actually Need It

Batch Apex exists for one job: processing record volumes too large for a single transaction, broken into scopes of up to 2,000 records per execution. You implement Database.Batchable, with a start method that defines the record set (usually a QueryLocator), an execute method that runs per batch scope, and a finish method for cleanup or summary email.

The real advantage is governor limit isolation. Each batch scope gets its own fresh set of governor limits: its own 100 SOQL queries, its own 150 DML statements, its own heap. If you need to update 400,000 Account records with a formula that touches related Contacts, no single transaction survives that. Batch Apex is the only one of the three tools built to survive it.

Batch Apex also supports Database.Stateful, which lets you accumulate data across batch executions, useful for building a summary total or error log across the entire run rather than resetting per scope.

The tradeoff is overhead. Batch jobs are slower to start, harder to debug, and limited to 5 active or queued batch jobs per org at once (100 if triggered by Flow or a scheduled job in specific configurations, but do not assume that headroom exists in a busy org). If your dataset is under a few thousand records, Batch Apex is overkill. Queueable handles it with less ceremony and no QueryLocator ceremony.

Choosing Correctly: A Direct Comparison

FactorFutureQueueableBatch
Complex parameters (sObjects)NoYesYes
Job status trackingNoYesYes
ChainingNoYes, up to 5 deepYes, via finish method
Best record volumeSingle callout, small dataUnder ~10,000 records10,000 to millions
Governor limit reset per scopeNoNoYes
Startup overheadLowLowHigher

Read that table plainly: Queueable wins on almost every column except raw volume and per-scope limit resets. That is not a coincidence. Salesforce built Queueable specifically to take Future's job and do it better, while leaving Batch alone as the tool for genuinely large data volumes.

A common mistake is using Batch Apex for a job of 3,000 records because a tutorial from years ago said so. That job runs fine in Queueable, with one execute call, no QueryLocator boilerplate, and clean status tracking. Reach for Batch only when the record count or the governor-limit-per-scope requirement actually forces your hand.

Chaining, Limits, and the Mistake Everyone Makes

The mistake that shows up most in code reviews is treating async Apex as a way to dodge limits entirely, rather than a way to reshape when limits apply. All three tools still count against your org's daily async Apex execution limits. Queueable and Future share a combined daily cap. Batch jobs share the 5 concurrent job cap. None of this is infinite.

Chaining makes this worse if you are not careful. A Queueable job that enqueues another Queueable job inside a loop, instead of once at the end of execute, will blow past the chain depth limit fast and throw a runtime exception in production, not in a sandbox test where volumes are small.

The fix is architectural, not a code trick: design each async unit of work to do one clear job, pass forward only what the next step needs, and stop chaining once the workflow is actually done. If you find yourself chaining Queueable jobs five, six, seven times to process a growing dataset, that is a sign the job belongs in Batch Apex instead, where the platform is built to iterate over large volumes without you manually re-enqueueing.

Pick based on data volume and complexity, not habit. Future is legacy. Queueable is the default. Batch is the specialist tool for genuinely large jobs. Most orgs need the first two and rarely touch the third.

Frequently Asked Questions

Can Queueable Apex fully replace Future methods?

For almost every use case, yes. Queueable Apex supports complex parameters like sObjects, provides job status tracking through AsyncApexJob, and allows chaining, none of which Future methods support. The only scenario where Future still has a narrow edge is a very simple callout fired from inside a trigger, where the extra overhead of a Queueable class is not worth it.

How many records can Queueable Apex handle before I need Batch Apex?

There is no hard record cap on Queueable Apex itself, but it runs in a single transaction with standard governor limits, so anything beyond a few thousand records tends to hit SOQL or DML ceilings. Once your dataset regularly exceeds around 10,000 records or needs governor limits reset per chunk, Batch Apex becomes the safer choice.

What is the maximum chain depth for Queueable Apex?

Queueable jobs can chain up to 5 levels deep in most Salesforce orgs, meaning one job can enqueue another, which enqueues another, up to that limit. Some orgs with specific license types have unlimited chaining, but production code should never assume that headroom exists.

Does Batch Apex reset governor limits for every batch?

Yes. Each execute method invocation in a Batch Apex job gets its own fresh governor limits, including a new allotment of SOQL queries and DML statements. This is the main reason Batch Apex can process millions of records where a single Queueable or Future transaction cannot.

Why does my Queueable job fail with a limit exception in production but not in testing?

This usually happens when a Queueable job enqueues another job inside a loop instead of once at the end of the execute method, causing the chain depth limit or the daily async job limit to be exceeded at real production data volumes. Sandbox and unit tests often run with small datasets that never trigger the same volume of chained jobs.